Compare commits
2 Commits
59f5f5e668
...
glassmorph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d0bf4cb1c | ||
|
|
2b8b9a84a3 |
@@ -130,6 +130,7 @@ dependencies {
|
||||
implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.core.ktx)
|
||||
implementation(libs.androidx.compose.foundation.layout)
|
||||
ksp(libs.room.compiler) // CHANGED: Use ksp instead of implementation
|
||||
|
||||
// Networking
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<StatusAction>()
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -35,7 +35,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -159,14 +159,14 @@ private fun MenuItem(
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.shadow(elevation = 8.dp, shape = ComponentDefaults.DefaultShape)
|
||||
.glassmorphic(shape = RoundedCornerShape(16.dp), alpha = 0.4f)
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainer
|
||||
color = Color.Transparent // Allow glassmorphic modifier to handle color
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
@@ -196,16 +196,4 @@ private fun MenuItem(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MenuItemPreview() {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
MenuItem(
|
||||
text = "Menu Item",
|
||||
imageVector = AppIcons.Add,
|
||||
painter = null,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
@@ -69,10 +69,8 @@ fun <T : TabItem> AppTabLayout(
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 8.dp)
|
||||
.height(56.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
shape = ComponentDefaults.CardShape
|
||||
)
|
||||
// Replace background with glassmorphic extension
|
||||
.glassmorphic(shape = ComponentDefaults.CardShape, alpha = 0.3f)
|
||||
) {
|
||||
val tabWidth = maxWidth / tabs.size
|
||||
|
||||
@@ -89,7 +87,7 @@ fun <T : TabItem> AppTabLayout(
|
||||
.fillMaxHeight()
|
||||
.padding(4.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f), // Glassy indicator
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -25,6 +25,8 @@ 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.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -41,16 +43,23 @@ fun AppTopAppBar(
|
||||
onNavigateBack: (() -> Unit)? = null,
|
||||
navigationIcon: @Composable (() -> Unit)? = null,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
|
||||
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = Color.Transparent
|
||||
),
|
||||
hintContent: Hint? = null
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
|
||||
TopAppBar(
|
||||
modifier = modifier.height(56.dp),
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
colors = colors,
|
||||
Surface(
|
||||
modifier = modifier.glassmorphic(shape = RectangleShape, alpha = 0.2f),
|
||||
color = Color.Transparent
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = Modifier.height(56.dp),
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
colors = colors,
|
||||
title = {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
@@ -102,8 +111,9 @@ fun AppTopAppBar(
|
||||
// No navigation icon
|
||||
}
|
||||
},
|
||||
actions = actions
|
||||
)
|
||||
actions = actions
|
||||
)
|
||||
}
|
||||
|
||||
if (showBottomSheet) {
|
||||
HintBottomSheet(
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
@@ -28,6 +29,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -100,24 +102,25 @@ fun BottomNavigationBar(
|
||||
targetOffsetY = { it }
|
||||
)
|
||||
) {
|
||||
|
||||
val baseHeight = if (showLabels) 80.dp else 56.dp
|
||||
val density = LocalDensity.current
|
||||
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
||||
val height = baseHeight + navBarDp
|
||||
|
||||
NavigationBar(
|
||||
modifier = modifier.height(height),
|
||||
containerColor = MaterialTheme.colorScheme.surface, // Cleaner look than surfaceVariant
|
||||
tonalElevation = 8.dp, // Slight elevation for depth
|
||||
modifier = modifier
|
||||
.height(height)
|
||||
// Apply glassmorphism on the top corners
|
||||
.glassmorphic(shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), alpha = 0.35f),
|
||||
containerColor = Color.Transparent, // Let the glass shine through
|
||||
tonalElevation = 0.dp,
|
||||
) {
|
||||
screens.forEach { screen ->
|
||||
val isSelected = screen == selectedItem
|
||||
val title = stringResource(id = screen.title)
|
||||
|
||||
// 1. Spring Animation for the Icon Scale
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isSelected) 1.2f else 1.0f, // Subtle pop effect
|
||||
targetValue = if (isSelected) 1.2f else 1.0f,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
@@ -129,7 +132,7 @@ fun BottomNavigationBar(
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (!isSelected) {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 2. Tactile feedback
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onItemSelected(screen)
|
||||
}
|
||||
},
|
||||
@@ -145,17 +148,16 @@ fun BottomNavigationBar(
|
||||
}
|
||||
} else null,
|
||||
icon = {
|
||||
// 3. Crossfade between Outlined and Filled icons
|
||||
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
|
||||
Icon(
|
||||
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
|
||||
contentDescription = title,
|
||||
modifier = Modifier.scale(scale) // Apply the spring scale
|
||||
modifier = Modifier.scale(scale)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
indicatorColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f), // Glassy indicator
|
||||
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
selectedTextColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
|
||||
@@ -5,6 +5,8 @@ package eu.gaudian.translator.view.composable
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -43,6 +45,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.shadow
|
||||
@@ -55,49 +58,51 @@ import androidx.compose.ui.unit.dp
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.ui.theme.semanticColors
|
||||
import eu.gaudian.translator.view.composable.ComponentDefaults.DefaultElevation
|
||||
|
||||
|
||||
object ComponentDefaults {
|
||||
// Sizing
|
||||
val DefaultButtonHeight = 48.dp
|
||||
val CardPadding = 8.dp
|
||||
|
||||
// Elevation
|
||||
val DefaultElevation = 0.dp
|
||||
val NoElevation = 0.dp
|
||||
|
||||
// Borders
|
||||
val DefaultBorderWidth = 1.dp
|
||||
|
||||
// Shapes
|
||||
val DefaultCornerRadius = 16.dp
|
||||
val CardClipRadius = 8.dp
|
||||
val CardClipRadius = 16.dp // Increased slightly for softer glass look
|
||||
val NoRounding = 0.dp
|
||||
val DefaultShape = RoundedCornerShape(DefaultCornerRadius)
|
||||
val CardClipShape = RoundedCornerShape(CardClipRadius)
|
||||
val CardShape = RoundedCornerShape(DefaultCornerRadius)
|
||||
val NoShape = RoundedCornerShape(NoRounding)
|
||||
|
||||
// Opacity Levels
|
||||
const val ALPHA_HIGH = 0.6f
|
||||
const val ALPHA_MEDIUM = 0.5f
|
||||
const val ALPHA_LOW = 0.3f
|
||||
const val ALPHA_MEDIUM = 0.4f
|
||||
const val ALPHA_LOW = 0.2f // Adjusted for glass
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A styled card container for displaying content with a consistent floating look.
|
||||
*
|
||||
* @param modifier The modifier to be applied to the card.
|
||||
* @param content The content to be displayed inside the card.
|
||||
* Standard Glassmorphism Modifier
|
||||
*/
|
||||
fun Modifier.glassmorphic(
|
||||
shape: Shape = ComponentDefaults.DefaultShape,
|
||||
alpha: Float = ComponentDefaults.ALPHA_LOW,
|
||||
borderAlpha: Float = 0.15f
|
||||
): Modifier = composed {
|
||||
this
|
||||
.shadow(elevation = 8.dp, shape = shape, spotColor = Color.Black.copy(alpha = 0.05f))
|
||||
.clip(shape)
|
||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = alpha))
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = borderAlpha),
|
||||
shape = shape
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppCard(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String? = null,
|
||||
icon: ImageVector? = null, // New optional icon parameter
|
||||
icon: ImageVector? = null,
|
||||
text: String? = null,
|
||||
expandable: Boolean = false,
|
||||
initiallyExpanded: Boolean = false,
|
||||
@@ -110,25 +115,17 @@ fun AppCard(
|
||||
label = "Chevron Rotation"
|
||||
)
|
||||
|
||||
// Check if we need to render the header row
|
||||
// Updated to include icon in the check
|
||||
val hasHeader = title != null || text != null || expandable || icon != null
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.shadow(
|
||||
DefaultElevation,
|
||||
shape = ComponentDefaults.CardShape
|
||||
)
|
||||
.clip(ComponentDefaults.CardClipShape)
|
||||
// Animate height changes when expanding/collapsing
|
||||
.glassmorphic(shape = ComponentDefaults.CardShape, alpha = 0.25f)
|
||||
.animateContentSize(),
|
||||
shape = ComponentDefaults.CardShape,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer
|
||||
color = Color.Transparent // Let glassmorphic handle the background
|
||||
) {
|
||||
Column {
|
||||
// --- Header Row ---
|
||||
if (hasHeader) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -137,7 +134,6 @@ fun AppCard(
|
||||
.padding(ComponentDefaults.CardPadding),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 1. Optional Icon on the left
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
@@ -148,7 +144,6 @@ fun AppCard(
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
|
||||
// 2. Title and Text Column
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
if (!title.isNullOrBlank()) {
|
||||
Text(
|
||||
@@ -157,12 +152,9 @@ fun AppCard(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
// Only show spacer if both title and text exist
|
||||
if (!title.isNullOrBlank() && !text.isNullOrBlank()) {
|
||||
Spacer(Modifier.size(4.dp))
|
||||
}
|
||||
|
||||
if (!text.isNullOrBlank()) {
|
||||
Text(
|
||||
text = text,
|
||||
@@ -172,7 +164,6 @@ fun AppCard(
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Expand Chevron (Far right)
|
||||
if (expandable) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowDropDown,
|
||||
@@ -184,15 +175,12 @@ fun AppCard(
|
||||
}
|
||||
}
|
||||
|
||||
// --- Content Area ---
|
||||
if (!expandable || isExpanded) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = ComponentDefaults.CardPadding,
|
||||
end = ComponentDefaults.CardPadding,
|
||||
bottom = ComponentDefaults.CardPadding,
|
||||
// If we have a header, remove the top padding so content sits closer to the title.
|
||||
// If no header (legacy behavior), keep the top padding.
|
||||
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
|
||||
),
|
||||
content = content
|
||||
@@ -304,31 +292,27 @@ fun AppButton(
|
||||
modifier: Modifier? = Modifier,
|
||||
enabled: Boolean = true,
|
||||
shape: Shape? = null,
|
||||
colors: ButtonColors = ButtonDefaults.buttonColors(),
|
||||
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
|
||||
colors: ButtonColors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) // Glassy primary
|
||||
),
|
||||
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(defaultElevation = 0.dp),
|
||||
border: BorderStroke? = null,
|
||||
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
|
||||
val m = modifier ?: Modifier.height(ComponentDefaults.DefaultButtonHeight)
|
||||
val s = shape ?: ComponentDefaults.DefaultShape
|
||||
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = m,
|
||||
modifier = m.border(1.dp, MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.2f), s),
|
||||
enabled = enabled,
|
||||
shape = s,
|
||||
colors = colors,
|
||||
elevation = elevation,
|
||||
border = border,
|
||||
contentPadding = PaddingValues(
|
||||
start = 8.dp, // More horizontal padding
|
||||
end = 8.dp,
|
||||
top = 8.dp, // Default vertical padding
|
||||
bottom = 8.dp
|
||||
),
|
||||
contentPadding = PaddingValues(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 8.dp),
|
||||
interactionSource = interactionSource
|
||||
) {
|
||||
content()
|
||||
@@ -368,11 +352,7 @@ fun AppOutlinedButton(
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PrimaryButtonWithIconPreview() {
|
||||
PrimaryButton(onClick = { }, text = stringResource(R.string.primary_with_icon), icon = AppIcons.Add)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The secondary button for less prominent actions.
|
||||
|
||||
@@ -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<VocabularyViewModel>(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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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)) }
|
||||
},
|
||||
|
||||
@@ -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<Application>().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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<string name="title_multiple">Mehrere</string>
|
||||
<string name="label_translation_settings">Übersetzung</string>
|
||||
<string name="reset_to_defaults">Auf Standard zurücksetzen</string>
|
||||
<string name="text_excel_not_supported_use_csv">Excel wird nicht unterstützt. Bitte CSV verwenden.</string>
|
||||
<string name="message_error_excel_not_supported">Excel wird nicht unterstützt. Bitte CSV verwenden.</string>
|
||||
<string name="error_parsing_table">Fehler beim Parsen der Tabelle</string>
|
||||
<string name="error_parsing_table_with_reason">Fehler beim Parsen der Tabelle: %1$s</string>
|
||||
<string name="label_import_table_csv_excel">Tabelle importieren (CSV)</string>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<string name="title_multiple">Múltiplos</string>
|
||||
<string name="label_translation_settings">Configurações de Tradução</string>
|
||||
<string name="reset_to_defaults">Restaurar Padrões</string>
|
||||
<string name="text_excel_not_supported_use_csv">Excel não é suportado. Use CSV.</string>
|
||||
<string name="message_error_excel_not_supported">Excel não é suportado. Use CSV.</string>
|
||||
<string name="error_parsing_table">Erro ao analisar tabela</string>
|
||||
<string name="error_parsing_table_with_reason">Erro ao analisar tabela: %1$s</string>
|
||||
<string name="label_import_table_csv_excel">Importar Tabela (CSV)</string>
|
||||
|
||||
@@ -788,7 +788,7 @@
|
||||
<string name="text_error_generating_questions">Error generating questions: %1$s</string>
|
||||
<string name="text_error_loading_stored_values">Error loading stored values: %1$s</string>
|
||||
<string name="text_error_saving_entry">Error saving entry: %1$s</string>
|
||||
<string name="text_excel_not_supported_use_csv">Excel is not supported. Use CSV instead.</string>
|
||||
<string name="message_error_excel_not_supported">Excel is not supported. Use CSV instead.</string>
|
||||
<string name="text_expand_widget">Expand Widget</string>
|
||||
<string name="text_explanation">Explanation</string>
|
||||
<string name="text_export_category">Export Category</string>
|
||||
@@ -1043,4 +1043,75 @@
|
||||
<string name="text_select">Select</string>
|
||||
<string name="text_search">Search</string>
|
||||
<string name="text_language_settings_description">Set what languages you want to use in the app. Languages that are not activated will not appear in this app. You can also add your own language to the list, or change an existing language (region/locale)</string>
|
||||
|
||||
<!-- Status Messages (for internationalization) -->
|
||||
<string name="message_success_generic">Success!</string>
|
||||
<string name="message_info_generic">Info</string>
|
||||
<string name="message_error_generic">An error occurred</string>
|
||||
<string name="message_loading_generic">Loading…</string>
|
||||
|
||||
<!-- Language related -->
|
||||
<string name="message_error_language_not_selected">Source and target languages must be selected.</string>
|
||||
<string name="message_error_no_words_found">No words found in the provided text.</string>
|
||||
<string name="message_success_language_replaced">Language ID updated for %1$d items.</string>
|
||||
|
||||
<!-- Vocabulary related -->
|
||||
<string name="message_success_vocabulary_imported">Vocabulary items imported successfully.</string>
|
||||
<string name="message_error_vocabulary_import_failed">Error importing vocabulary items: %1$s</string>
|
||||
<string name="message_success_items_merged">Items merged!</string>
|
||||
<string name="message_success_items_added">Successfully added %1$d new vocabulary items.</string>
|
||||
<string name="message_error_items_add_failed">Error adding items: %1$s</string>
|
||||
<string name="message_success_items_deleted">Successfully deleted vocabulary items.</string>
|
||||
<string name="message_error_items_delete_failed">Error deleting items: %1$s</string>
|
||||
<string name="message_error_no_cards_found">No cards found for the specified filter.</string>
|
||||
<string name="message_success_cards_loaded">Successfully loaded card set.</string>
|
||||
|
||||
<!-- Grammar related -->
|
||||
<string name="message_success_grammar_updated">Grammar details updated!</string>
|
||||
<string name="message_error_grammar_fetch_failed">Could not retrieve grammar details.</string>
|
||||
<string name="message_loading_grammar_fetch">Fetching grammar for %1$d items…</string>
|
||||
|
||||
<!-- File operations -->
|
||||
<string name="message_success_file_saved">File saved to %1$s</string>
|
||||
<string name="message_error_file_save_failed">Error saving file: %1$s</string>
|
||||
<string name="message_error_file_save_cancelled">File save cancelled or failed.</string>
|
||||
<string name="message_error_file_picker_not_initialized">Save File Launcher not initialized.</string>
|
||||
<string name="message_success_category_saved">Category saved to %1$s</string>
|
||||
|
||||
<!-- API Key related -->
|
||||
<string name="message_error_api_key_missing">API Key is missing or invalid.</string>
|
||||
<string name="message_error_api_key_invalid">API Key is missing or invalid.</string>
|
||||
|
||||
<!-- Translation related -->
|
||||
<string name="message_loading_translating">Translating %1$d words…</string>
|
||||
<string name="message_success_translation_completed">Translation completed.</string>
|
||||
<string name="message_error_translation_failed">Translation failed: %1$s</string>
|
||||
|
||||
<!-- Repository operations -->
|
||||
<string name="message_success_repository_wiped">All repository data deleted.</string>
|
||||
<string name="message_error_repository_wipe_failed">Failed to wipe repository: %1$s</string>
|
||||
<string name="message_loading_card_set">Loading card set</string>
|
||||
|
||||
<!-- Stage operations -->
|
||||
<string name="message_success_stage_updated">Stage updated successfully.</string>
|
||||
<string name="message_error_stage_update_failed">Error updating stage: %1$s</string>
|
||||
|
||||
<!-- Category operations -->
|
||||
<string name="message_success_category_updated">Category updated successfully.</string>
|
||||
<string name="message_error_category_update_failed">Error updating category: %1$s</string>
|
||||
|
||||
<!-- Article removal -->
|
||||
<string name="message_success_articles_removed">Articles removed successfully.</string>
|
||||
<string name="message_error_articles_remove_failed">Error removing articles: %1$s</string>
|
||||
|
||||
<!-- Synonyms -->
|
||||
<string name="message_success_synonyms_generated">Synonyms generated successfully.</string>
|
||||
<string name="message_error_synonyms_generation_failed">Failed to generate synonyms: %1$s</string>
|
||||
|
||||
<!-- Operation status -->
|
||||
<string name="message_error_operation_failed">Operation failed: %1$s</string>
|
||||
<string name="message_loading_operation_in_progress">Operation in progress…</string>
|
||||
<string name="message_test_info">This is a generic info message.</string>
|
||||
<string name="message_test_success">This is a test success message!</string>
|
||||
<string name="message_test_error">Oops, something went wrong :(</string>
|
||||
</resources>
|
||||
|
||||
@@ -43,6 +43,7 @@ truth = "1.4.5"
|
||||
zstdJni = "1.5.7-7"
|
||||
composeMarkdown = "0.5.8"
|
||||
jitpack = "1.0.10"
|
||||
foundationLayoutVersion = "1.10.3"
|
||||
|
||||
|
||||
[libraries]
|
||||
@@ -103,6 +104,7 @@ 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-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayoutVersion" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user