diff --git a/app/src/main/java/eu/gaudian/translator/utils/StatusMessageIds.kt b/app/src/main/java/eu/gaudian/translator/utils/StatusMessageIds.kt new file mode 100644 index 0000000..b0a01a9 --- /dev/null +++ b/app/src/main/java/eu/gaudian/translator/utils/StatusMessageIds.kt @@ -0,0 +1,116 @@ +package eu.gaudian.translator.utils + +import eu.gaudian.translator.R +import eu.gaudian.translator.viewmodel.MessageAction +import eu.gaudian.translator.viewmodel.MessageDisplayType + +/** + * Simplified status message IDs for internationalization. + * Each ID stores its metadata directly, reducing repetitive mapping code. + */ +enum class StatusMessageId( + val stringResId: Int, + val defaultType: MessageDisplayType, + val defaultTimeout: Int, + val associatedAction: MessageAction? = null +) { + // Generic messages + SUCCESS_GENERIC(R.string.message_success_generic, MessageDisplayType.SUCCESS, 3), + INFO_GENERIC(R.string.message_info_generic, MessageDisplayType.INFO, 3), + ERROR_GENERIC(R.string.message_error_generic, MessageDisplayType.ERROR, 5), + LOADING_GENERIC(R.string.message_loading_generic, MessageDisplayType.LOADING, 0), + TEST_INFO(R.string.message_test_info, MessageDisplayType.INFO, 3), + TEST_SUCCESS(R.string.message_test_success, MessageDisplayType.SUCCESS, 3), + TEST_ERROR(R.string.message_test_error, MessageDisplayType.ERROR, 5), + + + + + + // Language related + ERROR_LANGUAGE_NOT_SELECTED(R.string.message_error_language_not_selected, MessageDisplayType.ERROR, 5), + ERROR_NO_WORDS_FOUND(R.string.message_error_no_words_found, MessageDisplayType.ERROR, 5), + SUCCESS_LANGUAGE_REPLACED(R.string.message_success_language_replaced, MessageDisplayType.SUCCESS, 3), + + // Vocabulary related + SUCCESS_VOCABULARY_IMPORTED(R.string.message_success_vocabulary_imported, MessageDisplayType.SUCCESS, 3), + ERROR_VOCABULARY_IMPORT_FAILED(R.string.message_error_vocabulary_import_failed, MessageDisplayType.ERROR, 5), + SUCCESS_ITEMS_MERGED(R.string.message_success_items_merged, MessageDisplayType.SUCCESS, 3), + SUCCESS_ITEMS_ADDED(R.string.message_success_items_added, MessageDisplayType.SUCCESS, 3), + ERROR_ITEMS_ADD_FAILED(R.string.message_error_items_add_failed, MessageDisplayType.ERROR, 5), + SUCCESS_ITEMS_DELETED(R.string.message_success_items_deleted, MessageDisplayType.SUCCESS, 3), + ERROR_ITEMS_DELETE_FAILED(R.string.message_error_items_delete_failed, MessageDisplayType.ERROR, 5), + ERROR_NO_CARDS_FOUND(R.string.message_error_no_cards_found, MessageDisplayType.ERROR, 5), + SUCCESS_CARDS_LOADED(R.string.message_success_cards_loaded, MessageDisplayType.SUCCESS, 3), + + // Grammar related + SUCCESS_GRAMMAR_UPDATED(R.string.message_success_grammar_updated, MessageDisplayType.SUCCESS, 3), + ERROR_GRAMMAR_FETCH_FAILED(R.string.message_error_grammar_fetch_failed, MessageDisplayType.ERROR, 5), + LOADING_GRAMMAR_FETCH(R.string.message_loading_grammar_fetch, MessageDisplayType.LOADING, 0), + + // File operations + SUCCESS_FILE_SAVED(R.string.message_success_file_saved, MessageDisplayType.SUCCESS, 3), + ERROR_FILE_SAVE_FAILED(R.string.message_error_file_save_failed, MessageDisplayType.ERROR, 5), + ERROR_FILE_SAVE_CANCELLED(R.string.message_error_file_save_cancelled, MessageDisplayType.ERROR, 5), + 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), + + + // API Key related + ERROR_API_KEY_MISSING(R.string.message_error_api_key_missing, MessageDisplayType.ERROR, 0, MessageAction.NAVIGATE_TO_API_KEYS), + ERROR_API_KEY_INVALID(R.string.message_error_api_key_invalid, MessageDisplayType.ERROR, 0, MessageAction.NAVIGATE_TO_API_KEYS), + + // Translation related + LOADING_TRANSLATING(R.string.message_loading_translating, MessageDisplayType.LOADING, 0), + SUCCESS_TRANSLATION_COMPLETED(R.string.message_success_translation_completed, MessageDisplayType.SUCCESS, 3), + ERROR_TRANSLATION_FAILED(R.string.message_error_translation_failed, MessageDisplayType.ERROR, 5), + + // Repository operations + SUCCESS_REPOSITORY_WIPED(R.string.message_success_repository_wiped, MessageDisplayType.SUCCESS, 3), + ERROR_REPOSITORY_WIPE_FAILED(R.string.message_error_repository_wipe_failed, MessageDisplayType.ERROR, 5), + LOADING_CARD_SET(R.string.message_loading_card_set, MessageDisplayType.LOADING, 0), + + // Stage operations + SUCCESS_STAGE_UPDATED(R.string.message_success_stage_updated, MessageDisplayType.SUCCESS, 3), + ERROR_STAGE_UPDATE_FAILED(R.string.message_error_stage_update_failed, MessageDisplayType.ERROR, 5), + + // Category operations + SUCCESS_CATEGORY_UPDATED(R.string.message_success_category_updated, MessageDisplayType.SUCCESS, 3), + ERROR_CATEGORY_UPDATE_FAILED(R.string.message_error_category_update_failed, MessageDisplayType.ERROR, 5), + + // Article removal + SUCCESS_ARTICLES_REMOVED(R.string.message_success_articles_removed, MessageDisplayType.SUCCESS, 3), + ERROR_ARTICLES_REMOVE_FAILED(R.string.message_error_articles_remove_failed, MessageDisplayType.ERROR, 5), + + // Synonyms + SUCCESS_SYNONYMS_GENERATED(R.string.message_success_synonyms_generated, MessageDisplayType.SUCCESS, 3), + ERROR_SYNONYMS_GENERATION_FAILED(R.string.message_error_synonyms_generation_failed, MessageDisplayType.ERROR, 5), + + // Operation status + ERROR_OPERATION_FAILED(R.string.message_error_operation_failed, MessageDisplayType.ERROR, 5), + LOADING_OPERATION_IN_PROGRESS(R.string.message_loading_operation_in_progress, MessageDisplayType.LOADING, 0); + + companion object { + /** + * Convenience function to get the string resource ID from a StatusMessageId. + * Kept for backward compatibility with existing code. + */ + fun StatusMessageId.getStringResId(): Int = this.stringResId + + /** + * Convenience function to get the default display type. + */ + fun StatusMessageId.getDefaultDisplayType(): MessageDisplayType = this.defaultType + + /** + * Convenience function to get the default timeout. + */ + fun StatusMessageId.getDefaultTimeoutSeconds(): Int = this.defaultTimeout + + /** + * Convenience function to get the associated action. + */ + fun StatusMessageId.getAssociatedAction(): MessageAction? = this.associatedAction + } +} 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 8fa8d14..524a410 100644 --- a/app/src/main/java/eu/gaudian/translator/utils/StatusMessageService.kt +++ b/app/src/main/java/eu/gaudian/translator/utils/StatusMessageService.kt @@ -11,8 +11,10 @@ import kotlinx.coroutines.launch /** * A sealed class representing all possible actions that can be sent to the status system. + * Supports both legacy string-based messages and new ID-based messages for internationalization. */ sealed class StatusAction { + // Legacy string-based actions (deprecated in favor of ID-based actions) data class ShowMessage(val text: String, val type: MessageDisplayType, val timeoutInSeconds: Int) : StatusAction() data class ShowPermanentMessage(val text: String, val type: MessageDisplayType) : StatusAction() object CancelPermanentMessage : StatusAction() @@ -20,31 +22,59 @@ sealed class StatusAction { object CancelLoadingOperation : StatusAction() object HideMessageBar : StatusAction() object CancelAllMessages : StatusAction() - data class ShowActionableMessage(val text: String, val type: MessageDisplayType, val action: MessageAction) : StatusAction() + + // New ID-based actions for internationalization + data class ShowMessageById( + val messageId: StatusMessageId, + val type: MessageDisplayType = messageId.defaultType, + val timeoutInSeconds: Int = messageId.defaultTimeout + ) : StatusAction() + data class ShowPermanentMessageById( + val messageId: StatusMessageId, + val type: MessageDisplayType = messageId.defaultType + ) : StatusAction() + data class ShowActionableMessageById( + val messageId: StatusMessageId, + val type: MessageDisplayType = messageId.defaultType, + val action: MessageAction = messageId.associatedAction ?: MessageAction.NAVIGATE_TO_API_KEYS + ) : StatusAction() } /** * A singleton object that acts as a central event bus for status messages. * Any part of the app can trigger an action, and any StatusViewModel listening will receive it. + * + * NOTE: All message display requests should go through this service. */ object StatusMessageService { private val _actions = MutableSharedFlow() val actions = _actions.asSharedFlow() private val scope = CoroutineScope(Dispatchers.Default) - suspend fun trigger(action: StatusAction) { + /** + * Triggers a status action. This is the primary way to display messages. + * Internally launches a coroutine, so this function is not suspend. + */ + fun trigger(action: StatusAction) { Log.d("StatusMessageService", "Received action: $action") - _actions.emit(action) - } - - fun triggerNonSuspend(action: StatusAction) { - Log.d("StatusMessageService", "Received non-suspend action: $action") scope.launch { _actions.emit(action) } } + /** + * @deprecated Use trigger() instead. + */ + @Deprecated("Use trigger() instead", ReplaceWith("trigger(action)")) + fun triggerNonSuspend(action: StatusAction) { + trigger(action) + } + + /** + * @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 { @@ -52,6 +82,10 @@ object StatusMessageService { } } + /** + * @deprecated Use showErrorById() instead for internationalization support. + */ + @Deprecated("Use showErrorById() for internationalization support", ReplaceWith("showErrorById(messageId)")) fun showErrorMessage(text: String, timeoutInSeconds: Int = 5) { scope.launch { _actions.emit(StatusAction.ShowMessage( @@ -62,6 +96,10 @@ object StatusMessageService { } } + /** + * @deprecated Use showLoadingById() instead for internationalization support. + */ + @Deprecated("Use showLoadingById() for internationalization support", ReplaceWith("showLoadingById(messageId)")) fun showLoadingMessage(text: String, timeoutInSeconds: Int = 0) { scope.launch { _actions.emit(StatusAction.ShowMessage( @@ -71,6 +109,10 @@ object StatusMessageService { } } + /** + * @deprecated Use showInfoById() instead for internationalization support. + */ + @Deprecated("Use showInfoById() for internationalization support", ReplaceWith("showInfoById(messageId)")) fun showInfoMessage(text: String, timeoutInSeconds: Int = 3) { scope.launch { _actions.emit(StatusAction.ShowMessage( @@ -80,6 +122,10 @@ object StatusMessageService { } } + /** + * @deprecated Use showSuccessById() instead for internationalization support. + */ + @Deprecated("Use showSuccessById() for internationalization support", ReplaceWith("showSuccessById(messageId)")) fun showSuccessMessage(text: String, timeoutInSeconds: Int = 3) { scope.launch { _actions.emit(StatusAction.ShowMessage( @@ -89,33 +135,102 @@ object StatusMessageService { } } + /** + * @deprecated Use showPermanentMessageById() instead for internationalization support. + */ + @Deprecated("Use showPermanentMessageById() for internationalization support", ReplaceWith("showPermanentMessageById(messageId)")) fun showPermanentMessage(text: String, type: MessageDisplayType) { scope.launch { _actions.emit(StatusAction.ShowPermanentMessage(text, type)) } } + /** + * @deprecated Use StatusAction.CancelPermanentMessage via trigger() if needed. + */ + @Deprecated("Use StatusAction.CancelPermanentMessage via trigger() if needed") fun cancelPermanentMessage() { - scope.launch { - _actions.emit(StatusAction.CancelPermanentMessage) - } + trigger(StatusAction.CancelPermanentMessage) } + /** + * @deprecated Use StatusAction.HideMessageBar via trigger() if needed. + */ + @Deprecated("Use StatusAction.HideMessageBar via trigger() if needed") fun hideMessageBar() { - scope.launch { - _actions.emit(StatusAction.HideMessageBar) - } + trigger(StatusAction.HideMessageBar) } + /** + * @deprecated Use StatusAction.CancelAllMessages via trigger() if needed. + */ + @Deprecated("Use StatusAction.CancelAllMessages via trigger() if needed") fun cancelAllMessages() { - scope.launch { - _actions.emit(StatusAction.CancelAllMessages) - } + trigger(StatusAction.CancelAllMessages) } + /** + * @deprecated Use showActionableMessageById() instead for internationalization support. + */ + @Deprecated("Use showActionableMessageById() for internationalization support", ReplaceWith("showActionableMessageById(messageId)")) fun showActionableMessage(text: String, type: MessageDisplayType, action: MessageAction) { scope.launch { _actions.emit(StatusAction.ShowActionableMessage(text, type, action)) } } -} \ No newline at end of file + + // === NEW ID-BASED METHODS (for internationalization) === + + /** + * Shows a message by its ID. The actual text is resolved by StatusViewModel using string resources. + * @param messageId The StatusMessageId that maps to a string resource + * @param type Optional override for the display type + * @param timeoutInSeconds Optional override for the timeout + */ + fun showMessageById( + messageId: StatusMessageId, + type: MessageDisplayType = messageId.defaultType, + timeoutInSeconds: Int = messageId.defaultTimeout + ) { + trigger(StatusAction.ShowMessageById(messageId, type, timeoutInSeconds)) + } + + /** + * Shows a permanent message (until dismissed) by its ID. + */ + fun showPermanentMessageById( + messageId: StatusMessageId, + type: MessageDisplayType = messageId.defaultType + ) { + trigger(StatusAction.ShowPermanentMessageById(messageId, type)) + } + + /** + * Shows an actionable message by its ID with an optional action. + */ + fun showActionableMessageById( + messageId: StatusMessageId, + type: MessageDisplayType = messageId.defaultType, + action: MessageAction = messageId.associatedAction ?: MessageAction.NAVIGATE_TO_API_KEYS + ) { + trigger(StatusAction.ShowActionableMessageById(messageId, type, action)) + } + + // Convenience methods for common message types + + fun showErrorById(messageId: StatusMessageId, timeoutInSeconds: Int = messageId.defaultTimeout) { + showMessageById(messageId, MessageDisplayType.ERROR, timeoutInSeconds) + } + + fun showSuccessById(messageId: StatusMessageId, timeoutInSeconds: Int = messageId.defaultTimeout) { + showMessageById(messageId, MessageDisplayType.SUCCESS, timeoutInSeconds) + } + + fun showInfoById(messageId: StatusMessageId, timeoutInSeconds: Int = messageId.defaultTimeout) { + showMessageById(messageId, MessageDisplayType.INFO, timeoutInSeconds) + } + + fun showLoadingById(messageId: StatusMessageId, timeoutInSeconds: Int = messageId.defaultTimeout) { + showMessageById(messageId, MessageDisplayType.LOADING, timeoutInSeconds) + } +} diff --git a/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt b/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt index 1131d52..212fe35 100644 --- a/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt +++ b/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt @@ -62,6 +62,8 @@ import eu.gaudian.translator.ui.theme.AllThemes import eu.gaudian.translator.ui.theme.ProvideSemanticColors import eu.gaudian.translator.ui.theme.buildColorScheme import eu.gaudian.translator.utils.Log +import eu.gaudian.translator.utils.StatusAction +import eu.gaudian.translator.utils.StatusMessageService import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.view.composable.AppAlertDialog import eu.gaudian.translator.view.composable.BottomNavigationBar @@ -153,6 +155,7 @@ fun TranslatorApp( val activity = LocalContext.current.findActivity() val statusViewModel: StatusViewModel = hiltViewModel(activity) + val statusMessageService = StatusMessageService val navController = rememberNavController() val statusState by statusViewModel.status.collectAsStateWithLifecycle() @@ -304,7 +307,7 @@ fun TranslatorApp( StatusMessageSystem( statusState = statusState, navController = navController, - onDismiss = { statusViewModel.hideMessageBar() }, + onDismiss = { statusMessageService.trigger(StatusAction.HideMessageBar) }, modifier = Modifier .fillMaxWidth() ) @@ -408,4 +411,6 @@ private fun AppTheme( content() } } + + } \ No newline at end of file diff --git a/app/src/main/java/eu/gaudian/translator/view/dialogs/AddVocabularyDialog.kt b/app/src/main/java/eu/gaudian/translator/view/dialogs/AddVocabularyDialog.kt index 34e302b..0265b84 100644 --- a/app/src/main/java/eu/gaudian/translator/view/dialogs/AddVocabularyDialog.kt +++ b/app/src/main/java/eu/gaudian/translator/view/dialogs/AddVocabularyDialog.kt @@ -41,6 +41,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import eu.gaudian.translator.R import eu.gaudian.translator.model.VocabularyItem import eu.gaudian.translator.ui.theme.ThemePreviews +import eu.gaudian.translator.utils.StatusMessageService import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.view.LocalConnectionConfigured import eu.gaudian.translator.view.composable.AppButton @@ -50,7 +51,6 @@ import eu.gaudian.translator.view.composable.AppOutlinedTextField import eu.gaudian.translator.view.composable.SourceLanguageDropdown import eu.gaudian.translator.view.composable.TargetLanguageDropdown import eu.gaudian.translator.viewmodel.LanguageViewModel -import eu.gaudian.translator.viewmodel.StatusViewModel import eu.gaudian.translator.viewmodel.TranslationViewModel import eu.gaudian.translator.viewmodel.VocabularyViewModel import kotlinx.coroutines.delay @@ -67,12 +67,12 @@ fun AddVocabularyDialog( showMultiple: Boolean = true ) { val activity = LocalContext.current.findActivity() - val statusViewModel: StatusViewModel = hiltViewModel(viewModelStoreOwner = activity) val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) val vocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity) val coroutineScope = rememberCoroutineScope() val translationViewModel: TranslationViewModel = hiltViewModel(viewModelStoreOwner = activity) val connectionConfigured = LocalConnectionConfigured.current + val statusMessageService = StatusMessageService @@ -186,7 +186,7 @@ fun AddVocabularyDialog( selectedTranslations.clear() } .onFailure { exception -> - statusViewModel.showErrorMessage( + statusMessageService.showErrorMessage( textFailedToGetTranslations + exception.message) } } diff --git a/app/src/main/java/eu/gaudian/translator/view/settings/AboutScreen.kt b/app/src/main/java/eu/gaudian/translator/view/settings/AboutScreen.kt index 2a2b140..ec9338a 100644 --- a/app/src/main/java/eu/gaudian/translator/view/settings/AboutScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/settings/AboutScreen.kt @@ -44,6 +44,9 @@ import androidx.navigation.NavController import eu.gaudian.translator.BuildConfig import eu.gaudian.translator.R import eu.gaudian.translator.ui.theme.ThemePreviews +import eu.gaudian.translator.utils.StatusAction +import eu.gaudian.translator.utils.StatusMessageId +import eu.gaudian.translator.utils.StatusMessageService import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.AppIcons @@ -52,7 +55,6 @@ import eu.gaudian.translator.view.composable.AppSwitch import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.SecondaryButton import eu.gaudian.translator.viewmodel.SettingsViewModel -import eu.gaudian.translator.viewmodel.StatusViewModel import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -371,7 +373,7 @@ private fun DeveloperOptions( val context = LocalContext.current val activity = context.findActivity() - val statusViewModel: StatusViewModel = hiltViewModel(viewModelStoreOwner = activity) + val statusMessageService = StatusMessageService val settingsViewModel: SettingsViewModel = hiltViewModel(viewModelStoreOwner = activity) @@ -419,39 +421,36 @@ private fun DeveloperOptions( onCheckedChange = { settingsViewModel.setExperimentalFeatures(it) } ) } - - val loadingText = stringResource(R.string.text_loading_3d) - val infoText = stringResource(R.string.text_sentence_this_is_an_info_message) - val successText = stringResource(R.string.text_success_em) - val errorText = stringResource(R.string.text_sentence_oops_something_went_wrong) - + + + SecondaryButton( - onClick = { statusViewModel.showLoadingMessage(loadingText) }, + onClick = { statusMessageService.showMessageById(StatusMessageId.LOADING_GENERIC) }, text = stringResource(R.string.text_show_loading), modifier = Modifier.fillMaxWidth() ) SecondaryButton( - onClick = { statusViewModel.cancelLoadingOperation() }, + onClick = { statusMessageService.trigger(StatusAction.CancelLoadingOperation)}, text = stringResource(R.string.text_cancel_loading), modifier = Modifier.fillMaxWidth() ) SecondaryButton( - onClick = { statusViewModel.showInfoMessage(infoText) }, + onClick = { statusMessageService.showInfoById(StatusMessageId.TEST_INFO) }, text = stringResource(R.string.text_show_info_message), modifier = Modifier.fillMaxWidth() ) SecondaryButton( - onClick = { statusViewModel.showSuccessMessage(successText) }, + onClick = { statusMessageService.showSuccessById(StatusMessageId.TEST_SUCCESS) }, text = stringResource(R.string.title_show_success_message), modifier = Modifier.fillMaxWidth() ) SecondaryButton( - onClick = { statusViewModel.showErrorMessage(errorText, 2) }, + onClick = { statusMessageService.showErrorById(StatusMessageId.TEST_ERROR) }, text = stringResource(R.string.text_show_error_message), modifier = Modifier.fillMaxWidth() ) SecondaryButton( - onClick = { statusViewModel.showApiKeyMissingMessage() }, + onClick = { statusMessageService.showErrorById(StatusMessageId.ERROR_API_KEY_MISSING) }, text = stringResource(R.string.show_api_key_missing_message), modifier = Modifier.fillMaxWidth() ) diff --git a/app/src/main/java/eu/gaudian/translator/view/settings/VocabularyRepositoryOptionsScreen.kt b/app/src/main/java/eu/gaudian/translator/view/settings/VocabularyRepositoryOptionsScreen.kt index 39b91ac..789081a 100644 --- a/app/src/main/java/eu/gaudian/translator/view/settings/VocabularyRepositoryOptionsScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/settings/VocabularyRepositoryOptionsScreen.kt @@ -39,6 +39,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import eu.gaudian.translator.R import eu.gaudian.translator.model.Language +import eu.gaudian.translator.utils.StatusMessageId +import eu.gaudian.translator.utils.StatusMessageService import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.view.composable.AppButton import eu.gaudian.translator.view.composable.AppCard @@ -50,7 +52,6 @@ import eu.gaudian.translator.view.composable.PrimaryButton import eu.gaudian.translator.view.composable.SecondaryButton import eu.gaudian.translator.view.composable.SingleLanguageDropDown import eu.gaudian.translator.viewmodel.LanguageViewModel -import eu.gaudian.translator.viewmodel.StatusViewModel import eu.gaudian.translator.viewmodel.VocabularyViewModel @Composable @@ -60,7 +61,7 @@ fun VocabularyRepositoryOptionsScreen( val activity = LocalContext.current.findActivity() val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity) - val statusViewModel: StatusViewModel = hiltViewModel(viewModelStoreOwner = activity) + val statusMessageService = StatusMessageService val context = LocalContext.current @@ -73,7 +74,7 @@ fun VocabularyRepositoryOptionsScreen( context.contentResolver.openInputStream(it)?.use { inputStream -> val jsonString = inputStream.bufferedReader().use { reader -> reader.readText() } vocabularyViewModel.importVocabulary(jsonString) - statusViewModel.showInfoMessage(repositoryStateImportedFrom + " " +it.path) + statusMessageService.showInfoMessage(repositoryStateImportedFrom + " " +it.path) } } } @@ -145,7 +146,7 @@ fun VocabularyRepositoryOptionsScreen( row.map { it.trim().trim('"') } }.filter { r -> r.any { it.isNotBlank() } } } - val textExcelNotSupportedUseCsv = stringResource(R.string.text_excel_not_supported_use_csv) + val errorParsingTable = stringResource(R.string.error_parsing_table) val errorParsingTableWithReason = stringResource(R.string.error_parsing_table_with_reason) val importTableLauncher = rememberLauncherForActivityResult( @@ -159,7 +160,7 @@ fun VocabularyRepositoryOptionsScreen( val mime = context.contentResolver.getType(u) val isExcel = mime == "application/vnd.ms-excel" || mime == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" if (isExcel) { - statusViewModel.showInfoMessage(textExcelNotSupportedUseCsv) + statusMessageService.showErrorById(StatusMessageId.ERROR_EXCEL_NOT_SUPPORTED) return@let } context.contentResolver.openInputStream(u)?.use { inputStream -> @@ -173,12 +174,12 @@ fun VocabularyRepositoryOptionsScreen( parseError = null } else { parseError = errorParsingTable - statusViewModel.showErrorMessage(parseError!!) + statusMessageService.showErrorMessage(parseError!!) } } } catch (e: Exception) { parseError = e.message - statusViewModel.showErrorMessage( + statusMessageService.showErrorMessage( (errorParsingTableWithReason + " " + e.message) ) } @@ -394,13 +395,13 @@ fun VocabularyRepositoryOptionsScreen( val infoImportedItemsFrom = stringResource(R.string.info_imported_items_from) TextButton(onClick = { if (selectedColFirst == selectedColSecond) { - statusViewModel.showErrorMessage(errorSelectTwoColumns) + statusMessageService.showErrorMessage(errorSelectTwoColumns) return@TextButton } val langA = selectedLangFirst val langB = selectedLangSecond if (langA == null || langB == null) { - statusViewModel.showErrorMessage(errorSelectLanguages) + statusMessageService.showErrorMessage(errorSelectLanguages) return@TextButton } val startIdx = if (skipHeader) 1 else 0 @@ -416,11 +417,11 @@ fun VocabularyRepositoryOptionsScreen( ) } if (items.isEmpty()) { - statusViewModel.showErrorMessage(errorNoRowsToImport) + statusMessageService.showErrorMessage(errorNoRowsToImport) return@TextButton } vocabularyViewModel.addVocabularyItems(items) - statusViewModel.showSuccessMessage(infoImportedItemsFrom + " " +items.size) + statusMessageService.showSuccessMessage(infoImportedItemsFrom + " " +items.size) showTableImportDialog.value = false }) { Text(stringResource(R.string.label_import)) } }, diff --git a/app/src/main/java/eu/gaudian/translator/viewmodel/StatusViewModel.kt b/app/src/main/java/eu/gaudian/translator/viewmodel/StatusViewModel.kt index d646c5a..2760719 100644 --- a/app/src/main/java/eu/gaudian/translator/viewmodel/StatusViewModel.kt +++ b/app/src/main/java/eu/gaudian/translator/viewmodel/StatusViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import eu.gaudian.translator.utils.Log import eu.gaudian.translator.utils.StatusAction +import eu.gaudian.translator.utils.StatusMessageId import eu.gaudian.translator.utils.StatusMessageService import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job @@ -45,6 +46,16 @@ enum class MessageDisplayType(val priority: Int) { ACTIONABLE_ERROR(5) } +/** + * StatusViewModel is responsible for: + * 1. Collecting status actions from StatusMessageService + * 2. Managing the message queue + * 3. Resolving StatusMessageId to actual strings + * 4. Managing status state + * + * NOTE: All message display requests should go through StatusMessageService. + * This ViewModel should NOT be called directly to display messages. + */ @HiltViewModel class StatusViewModel @Inject constructor( application: Application, @@ -67,9 +78,14 @@ class StatusViewModel @Inject constructor( } } + /** + * Handles all status actions from StatusMessageService. + * This is the main entry point for all status messages. + */ private fun handleAction(action: StatusAction) { Log.d("StatusViewModel", "Received action: $action") when (action) { + // Legacy string-based actions (deprecated but still supported for backward compatibility) is StatusAction.ShowMessage -> showMessageInternal(action.text, action.type, action.timeoutInSeconds) is StatusAction.ShowActionableMessage -> showPermanentActionableMessageInternal(action.text, action.type, action.action) is StatusAction.ShowPermanentMessage -> showPermanentMessageInternal(action.text, action.type) @@ -78,17 +94,67 @@ class StatusViewModel @Inject constructor( is StatusAction.CancelPermanentMessage -> cancelPermanentMessageInternal() is StatusAction.HideMessageBar -> hideMessageBarInternal() is StatusAction.CancelAllMessages -> cancelAllMessagesInternal() + + // New ID-based actions for internationalization + is StatusAction.ShowMessageById -> showMessageByIdInternal( + action.messageId, + action.type, + action.timeoutInSeconds + ) + is StatusAction.ShowPermanentMessageById -> showPermanentMessageByIdInternal( + action.messageId, + action.type + ) + is StatusAction.ShowActionableMessageById -> showPermanentActionableMessageByIdInternal( + action.messageId, + action.type, + action.action + ) } } - fun showApiKeyMissingMessage() = viewModelScope.launch { - statusMessageService.showActionableMessage( - text = "API Key is missing or invalid.", - type = MessageDisplayType.ACTIONABLE_ERROR, - action = MessageAction.NAVIGATE_TO_API_KEYS - ) + /** + * Resolves a StatusMessageId to its actual string text using Android string resources. + */ + private fun resolveMessageText(messageId: StatusMessageId): String { + return try { + getApplication().getString(messageId.stringResId) + } catch (e: Exception) { + Log.e("StatusViewModel", "Failed to resolve message string for ID: $messageId", e) + "Message not available" + } } + // --- ID-based internal methods --- + + private fun showMessageByIdInternal( + messageId: StatusMessageId, + type: MessageDisplayType, + timeoutInSeconds: Int + ) { + val text = resolveMessageText(messageId) + showMessageInternal(text, type, timeoutInSeconds) + } + + private fun showPermanentMessageByIdInternal( + messageId: StatusMessageId, + type: MessageDisplayType + ) { + val text = resolveMessageText(messageId) + showPermanentMessageInternal(text, type) + } + + private fun showPermanentActionableMessageByIdInternal( + messageId: StatusMessageId, + type: MessageDisplayType, + action: MessageAction + ) { + val text = resolveMessageText(messageId) + showPermanentActionableMessageInternal(text, type, action) + } + + // --- Internal message display methods --- + private fun showPermanentActionableMessageInternal(message: String, type: MessageDisplayType, action: MessageAction) { cancelAllOperations() // Clear any other messages or loaders. _status.value = StatusState.Message(messageIdCounter++, message, type, action) @@ -99,54 +165,6 @@ class StatusViewModel @Inject constructor( _status.value = StatusState.Message(messageIdCounter++, message, type, action = null) } - fun showPermanentMessage(message: String, type: MessageDisplayType) = viewModelScope.launch { - statusMessageService.showPermanentMessage(message, type) - } - - fun cancelPermanentMessage() = viewModelScope.launch { - statusMessageService.cancelPermanentMessage() - } - - fun performLoadingOperation(block: suspend () -> Unit) = viewModelScope.launch { - statusMessageService.trigger(StatusAction.PerformLoadingOperation(block)) - } - - fun cancelLoadingOperation() = viewModelScope.launch { - statusMessageService.trigger(StatusAction.CancelLoadingOperation) - } - - fun showInfoMessage(message: String, timeoutInSeconds: Int = 3) = viewModelScope.launch { - statusMessageService.showInfoMessage(message, timeoutInSeconds) - } - - fun showLoadingMessage(message: String, timeoutInSeconds: Int = 0) = viewModelScope.launch { // Default timeout 0 for indefinite - statusMessageService.showLoadingMessage(message, timeoutInSeconds) - } - - fun showErrorMessage(message: String, timeoutInSeconds: Int = 5) = viewModelScope.launch { // Default timeout 5 for errors - statusMessageService.showErrorMessage(message, timeoutInSeconds) - } - - fun showSuccessMessage(message: String, timeoutInSeconds: Int = 3) = viewModelScope.launch { - statusMessageService.showSuccessMessage(message, timeoutInSeconds) - } - - fun hideMessageBar() = viewModelScope.launch { - statusMessageService.hideMessageBar() - } - - fun cancelAllMessages() = viewModelScope.launch { - statusMessageService.cancelAllMessages() - } - - private fun cancelPermanentMessageInternal() { - if (_status.value is StatusState.Message) { - // This logic can be simplified or adjusted based on desired behavior for permanent messages - _status.value = StatusState.Hidden - processNextMessageInQueue() - } - } - private fun performLoadingOperationInternal(block: suspend () -> Unit) { cancelAllOperations() _status.value = StatusState.Loading @@ -159,7 +177,10 @@ class StatusViewModel @Inject constructor( Log.i("StatusViewModel", "Loading operation was cancelled.") } catch (e: Exception) { Log.e("StatusViewModel", "Loading operation failed.", e) - showErrorMessage("Operation failed: ${e.localizedMessage ?: "Unknown error"}") + // Trigger error message through StatusMessageService + viewModelScope.launch { + statusMessageService.showErrorById(StatusMessageId.ERROR_OPERATION_FAILED) + } } finally { if (activeLoadingJob == this.coroutineContext[Job]) { if (_status.value == StatusState.Loading) { @@ -181,7 +202,38 @@ class StatusViewModel @Inject constructor( } } - // --- REVISED LOGIC --- + private fun cancelPermanentMessageInternal() { + if (_status.value is StatusState.Message) { + _status.value = StatusState.Hidden + processNextMessageInQueue() + } + } + + private fun hideMessageBarInternal() { + messageDisplayJob?.cancel() + messageDisplayJob = null + if (_status.value is StatusState.Message) { + _status.value = StatusState.Hidden + } + if (activeLoadingJob?.isActive != true) { + processNextMessageInQueue() + } + } + + private fun cancelAllMessagesInternal() { + Log.d("StatusViewModel", "Cancelling all messages.") + messageQueue.clear() + messageDisplayJob?.cancel() + messageDisplayJob = null + if (_status.value is StatusState.Message) { + _status.value = StatusState.Hidden + } + } + + /** + * Displays a message with priority-based queuing. + * High-priority messages interrupt lower-priority ones. + */ private fun showMessageInternal(message: String, type: MessageDisplayType, timeoutInSeconds: Int) { val currentState = _status.value val currentPriority = (currentState as? StatusState.Message)?.type?.priority ?: -1 @@ -204,29 +256,6 @@ class StatusViewModel @Inject constructor( } } - private fun hideMessageBarInternal() { - messageDisplayJob?.cancel() - messageDisplayJob = null - if (_status.value is StatusState.Message) { - _status.value = StatusState.Hidden - } - if (activeLoadingJob?.isActive != true) { - processNextMessageInQueue() - } - } - - private fun cancelAllMessagesInternal() { - Log.d("StatusViewModel", "Cancelling all messages.") - messageQueue.clear() - messageDisplayJob?.cancel() - messageDisplayJob = null - // Do not cancel activeLoadingJob here unless that's the desired behavior. - // Assuming CancelAllMessages is for the message bar only. - if (_status.value is StatusState.Message) { - _status.value = StatusState.Hidden - } - } - private fun cancelAllOperations() { messageQueue.clear() messageDisplayJob?.cancel() @@ -236,7 +265,9 @@ class StatusViewModel @Inject constructor( _status.value = StatusState.Hidden } - // --- REVISED LOGIC --- + /** + * Processes the next message in the queue. + */ private fun processNextMessageInQueue(timeoutInSeconds: Int = 3) { if (activeLoadingJob?.isActive == true) { Log.d("StatusViewModel", "Full-screen loading is active, not processing message queue now.") @@ -278,4 +309,4 @@ class StatusViewModel @Inject constructor( Log.d("StatusViewModel", "onCleared called. Cancelling all operations.") cancelAllOperations() } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/gaudian/translator/viewmodel/VocabularyViewModel.kt b/app/src/main/java/eu/gaudian/translator/viewmodel/VocabularyViewModel.kt index 9f19a71..45f9d8a 100644 --- a/app/src/main/java/eu/gaudian/translator/viewmodel/VocabularyViewModel.kt +++ b/app/src/main/java/eu/gaudian/translator/viewmodel/VocabularyViewModel.kt @@ -26,6 +26,7 @@ import eu.gaudian.translator.model.repository.VocabularyFileSaver import eu.gaudian.translator.model.repository.VocabularyRepository import eu.gaudian.translator.utils.Log import eu.gaudian.translator.utils.StatusAction +import eu.gaudian.translator.utils.StatusMessageId import eu.gaudian.translator.utils.StatusMessageService import eu.gaudian.translator.utils.StringHelper import eu.gaudian.translator.utils.VocabularyService @@ -774,7 +775,7 @@ class VocabularyViewModel @Inject constructor( statusService.hideMessageBar() if (_cardSet.value == null) { statusService.cancelAllMessages() - statusService.showErrorMessage("No cards found for the specified filter", 3) + statusService.showErrorById(StatusMessageId.ERROR_NO_CARDS_FOUND) } } } diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index be160d6..ff9e2b9 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -41,7 +41,7 @@ Mehrere Übersetzung Auf Standard zurücksetzen - Excel wird nicht unterstützt. Bitte CSV verwenden. + Excel wird nicht unterstützt. Bitte CSV verwenden. Fehler beim Parsen der Tabelle Fehler beim Parsen der Tabelle: %1$s Tabelle importieren (CSV) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 50217a4..0d5cda0 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -41,7 +41,7 @@ Múltiplos Configurações de Tradução Restaurar Padrões - Excel não é suportado. Use CSV. + Excel não é suportado. Use CSV. Erro ao analisar tabela Erro ao analisar tabela: %1$s Importar Tabela (CSV) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ed75844..999655a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -788,7 +788,7 @@ Error generating questions: %1$s Error loading stored values: %1$s Error saving entry: %1$s - Excel is not supported. Use CSV instead. + Excel is not supported. Use CSV instead. Expand Widget Explanation Export Category @@ -1043,4 +1043,75 @@ 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) + + + Success! + Info + An error occurred + Loading… + + + Source and target languages must be selected. + No words found in the provided text. + Language ID updated for %1$d items. + + + Vocabulary items imported successfully. + Error importing vocabulary items: %1$s + Items merged! + Successfully added %1$d new vocabulary items. + Error adding items: %1$s + Successfully deleted vocabulary items. + Error deleting items: %1$s + No cards found for the specified filter. + Successfully loaded card set. + + + Grammar details updated! + Could not retrieve grammar details. + Fetching grammar for %1$d items… + + + File saved to %1$s + Error saving file: %1$s + File save cancelled or failed. + Save File Launcher not initialized. + Category saved to %1$s + + + API Key is missing or invalid. + API Key is missing or invalid. + + + Translating %1$d words… + Translation completed. + Translation failed: %1$s + + + All repository data deleted. + Failed to wipe repository: %1$s + Loading card set + + + Stage updated successfully. + Error updating stage: %1$s + + + Category updated successfully. + Error updating category: %1$s + + + Articles removed successfully. + Error removing articles: %1$s + + + Synonyms generated successfully. + Failed to generate synonyms: %1$s + + + Operation failed: %1$s + Operation in progress… + This is a generic info message. + This is a test success message! + Oops, something went wrong :(