1.0.0 - Material you rework

This commit is contained in:
2025-08-31 02:24:26 -06:00
parent e4ea44f766
commit d10622c382
52 changed files with 1689 additions and 975 deletions

View File

@@ -13,11 +13,10 @@ plugins {
android {
defaultConfig {
applicationId = "com.atridad.mealient"
versionCode = 38
versionName = "0.6.0"
versionCode = 39
versionName = "1.0.0"
testInstrumentationRunner = "com.atridad.mealient.MealientTestRunner"
testInstrumentationRunnerArguments += mapOf("clearPackageData" to "true")
resourceConfigurations += listOf("en", "es", "ru", "fr", "nl", "pt", "de")
}
signingConfigs {

View File

@@ -11,6 +11,8 @@ interface PreferencesStorage {
val lastExecutedMigrationVersionKey: Preferences.Key<Int>
val themeModeKey: Preferences.Key<String>
suspend fun <T> getValue(key: Preferences.Key<T>): T?
suspend fun <T> requireValue(key: Preferences.Key<T>): T

View File

@@ -29,6 +29,9 @@ class PreferencesStorageImpl @Inject constructor(
override val lastExecutedMigrationVersionKey: Preferences.Key<Int> =
intPreferencesKey("lastExecutedMigrationVersion")
override val themeModeKey: Preferences.Key<String> =
stringPreferencesKey("themeMode")
override suspend fun <T> getValue(key: Preferences.Key<T>): T? {
val value = dataStore.data.first()[key]
logger.v { "getValue() returned: $value for $key" }

View File

@@ -11,6 +11,7 @@ import com.atridad.mealient.ui.destinations.BaseURLScreenDestination
import com.atridad.mealient.ui.destinations.DisclaimerScreenDestination
import com.atridad.mealient.ui.destinations.RecipeScreenDestination
import com.atridad.mealient.ui.destinations.RecipesListDestination
import com.atridad.mealient.ui.destinations.SettingsScreenDestination
import com.mealient.user_management.ui.profile.destinations.UserProfileScreenDestination
internal object NavGraphs {
@@ -41,6 +42,7 @@ internal object NavGraphs {
DisclaimerScreenDestination,
BaseURLScreenDestination,
AuthenticationScreenDestination,
SettingsScreenDestination,
UserProfileScreenDestination,
),
nestedNavGraphs = listOf(

View File

@@ -4,8 +4,8 @@ import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.Logout
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.ShoppingCart
import androidx.compose.material.icons.filled.SyncAlt
@@ -80,7 +80,7 @@ internal fun createDrawerItems(
return listOf(
createNavigationItem(
nameRes = R.string.menu_navigation_drawer_recipes_list,
icon = Icons.Default.List,
icon = Icons.AutoMirrored.Filled.List,
direction = RecipesListDestination,
),
createNavigationItem(
@@ -105,7 +105,7 @@ internal fun createDrawerItems(
),
createActionItem(
nameRes = R.string.menu_navigation_drawer_logout,
icon = Icons.Default.Logout,
icon = Icons.AutoMirrored.Filled.Logout,
appEvent = AppEvent.Logout,
),
createActionItem(

View File

@@ -4,12 +4,16 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.runtime.collectAsState
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowInsetsControllerCompat
import dagger.hilt.android.AndroidEntryPoint
import com.atridad.mealient.extensions.isDarkThemeOn
import com.atridad.mealient.logging.Logger
import com.atridad.mealient.ui.AppTheme
import com.atridad.mealient.ui.theme.MealientTheme
import com.atridad.mealient.data.storage.PreferencesStorage
import com.atridad.mealient.ui.settings.ThemeMode
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
@AndroidEntryPoint
@@ -20,20 +24,33 @@ class MainActivity : ComponentActivity() {
private val viewModel by viewModels<MainActivityViewModel>()
@Inject lateinit var prefs: PreferencesStorage
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" }
// Status bar appearance is now handled by the Material 3 theme
// Navigation bar appearance can still be set here if needed
with(WindowInsetsControllerCompat(window, window.decorView)) {
val isAppearanceLightBars = !isDarkThemeOn()
isAppearanceLightNavigationBars = isAppearanceLightBars
isAppearanceLightStatusBars = isAppearanceLightBars
}
splashScreen.setKeepOnScreenCondition {
viewModel.appState.value.forcedRoute == ForcedDestination.Undefined
}
setContent {
AppTheme {
// Observe theme changes live from preferences
val initialMode = runBlocking { prefs.getValue(prefs.themeModeKey) } ?: ThemeMode.DEVICE.name
val themeName = prefs.valueUpdates(prefs.themeModeKey)
.collectAsState(initial = initialMode).value ?: initialMode
val selectedMode = runCatching { ThemeMode.valueOf(themeName) }.getOrDefault(ThemeMode.DEVICE)
val dark = when (selectedMode) {
ThemeMode.DEVICE -> androidx.compose.foundation.isSystemInDarkTheme()
ThemeMode.LIGHT -> false
ThemeMode.DARK -> true
}
MealientTheme(darkTheme = dark) {
MealientApp(viewModel)
}
}

View File

@@ -1,15 +1,36 @@
package com.atridad.mealient.ui.activity
import android.content.Intent
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.ShoppingCart
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.ShoppingCart
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.ui.graphics.luminance
import androidx.core.view.WindowCompat
import androidx.compose.ui.res.stringResource
import androidx.compose.foundation.layout.WindowInsets
import androidx.navigation.NavHostController
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.defaults.RootNavGraphDefaultAnimations
@@ -21,8 +42,15 @@ import com.ramcosta.composedestinations.spec.DestinationSpec
import com.ramcosta.composedestinations.spec.NavHostEngine
import com.ramcosta.composedestinations.spec.Route
import com.ramcosta.composedestinations.utils.currentDestinationAsState
import com.atridad.mealient.R
import com.atridad.mealient.ui.NavGraphs
import com.atridad.mealient.ui.components.rememberBaseScreenState
import com.atridad.mealient.ui.destinations.RecipesListDestination
import com.atridad.mealient.ui.destinations.AddRecipeScreenDestination
import com.atridad.mealient.ui.destinations.SettingsScreenDestination
import com.atridad.mealient.shopping_lists.ui.destinations.ShoppingListsScreenDestination
import com.mealient.user_management.ui.profile.destinations.UserProfileScreenDestination
import androidx.compose.ui.graphics.toArgb
@Composable
internal fun MealientApp(
@@ -49,6 +77,22 @@ private fun MealientApp(
val currentDestinationState = controller.currentDestinationAsState()
val currentDestination = currentDestinationState.value
// Ensure system bars match app colors
val view = LocalView.current
val barsColor = androidx.compose.material3.MaterialTheme.colorScheme.surface
// Match Android navigation bar to the BottomAppBar's elevated container color
val bottomBarColor = androidx.compose.material3.MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
// Decide icon appearance from actual nav bar color brightness to match app-selected theme
val lightBars = bottomBarColor.luminance() > 0.5f
androidx.compose.runtime.SideEffect {
val window = (view.context as android.app.Activity).window
window.navigationBarColor = bottomBarColor.toArgb()
window.statusBarColor = barsColor.toArgb()
val controller = WindowCompat.getInsetsController(window, view)
controller.isAppearanceLightNavigationBars = lightBars
controller.isAppearanceLightStatusBars = lightBars
}
ForceNavigationEffect(
currentDestination = currentDestination,
controller = controller,
@@ -94,38 +138,48 @@ private fun MealientDialog(
dialogState: DialogState,
onEvent: (AppEvent) -> Unit,
) {
AlertDialog(
androidx.compose.material3.AlertDialog(
onDismissRequest = {
onEvent(dialogState.onDismiss)
},
confirmButton = {
TextButton(
androidx.compose.material3.TextButton(
onClick = { onEvent(dialogState.onPositiveClick) },
) {
Text(
text = stringResource(id = dialogState.positiveButton),
style = androidx.compose.material3.MaterialTheme.typography.labelLarge
)
}
},
dismissButton = {
TextButton(
androidx.compose.material3.TextButton(
onClick = { onEvent(dialogState.onNegativeClick) },
) {
Text(
text = stringResource(id = dialogState.negativeButton),
style = androidx.compose.material3.MaterialTheme.typography.labelLarge
)
}
},
title = {
Text(
text = stringResource(id = dialogState.title),
style = androidx.compose.material3.MaterialTheme.typography.headlineSmall,
color = androidx.compose.material3.MaterialTheme.colorScheme.onSurface
)
},
text = {
Text(
text = stringResource(id = dialogState.message),
style = androidx.compose.material3.MaterialTheme.typography.bodyMedium,
color = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant
)
},
containerColor = androidx.compose.material3.MaterialTheme.colorScheme.surface,
titleContentColor = androidx.compose.material3.MaterialTheme.colorScheme.onSurface,
textContentColor = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant,
shape = androidx.compose.foundation.shape.RoundedCornerShape(28.dp)
)
}
@@ -157,22 +211,89 @@ private fun AppContent(
startRoute: Route?,
onEvent: (AppEvent) -> Unit,
) {
val drawerItems = createDrawerItems(
navController = controller,
onEvent = onEvent,
)
val baseScreenState = rememberBaseScreenState(
drawerItems = drawerItems,
)
val currentDestination by controller.currentDestinationAsState()
val view = LocalView.current
val bottomBarColor = androidx.compose.material3.MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
val lightNavIcons = bottomBarColor.luminance() > 0.5f
androidx.compose.runtime.SideEffect {
val window = (view.context as android.app.Activity).window
window.navigationBarColor = bottomBarColor.toArgb()
val controllerInsets = WindowCompat.getInsetsController(window, view)
controllerInsets.isAppearanceLightNavigationBars = lightNavIcons
}
Scaffold(
contentWindowInsets = WindowInsets(0.dp),
bottomBar = {
BottomAppBar(
windowInsets = WindowInsets(0.dp),
containerColor = bottomBarColor,
actions = {
NavigationBarItem(
icon = { Icon(Icons.AutoMirrored.Filled.List, contentDescription = "Recipes") },
label = { Text(stringResource(R.string.menu_navigation_drawer_recipes_list)) },
selected = currentDestination?.route == RecipesListDestination.route,
onClick = {
controller.navigate(RecipesListDestination) {
popUpTo(controller.graph.startDestinationId) {
inclusive = false
}
launchSingleTop = true
}
}
)
DestinationsNavHost(
navGraph = NavGraphs.root,
engine = engine,
navController = controller,
startRoute = startRoute ?: NavGraphs.root.startRoute,
dependenciesContainerBuilder = {
dependency(baseScreenState)
NavigationBarItem(
icon = { Icon(Icons.Default.ShoppingCart, contentDescription = "Shopping Lists") },
label = { Text(stringResource(R.string.menu_navigation_drawer_shopping_lists)) },
selected = currentDestination?.route == ShoppingListsScreenDestination.route,
onClick = {
controller.navigate(ShoppingListsScreenDestination) {
popUpTo(controller.graph.startDestinationId) {
inclusive = false
}
launchSingleTop = true
}
}
)
NavigationBarItem(
icon = { Icon(Icons.Default.Settings, contentDescription = "Settings") },
label = { Text("Settings") },
selected = currentDestination?.route == SettingsScreenDestination.route,
onClick = {
controller.navigate(SettingsScreenDestination) {
popUpTo(controller.graph.startDestinationId) {
inclusive = false
}
launchSingleTop = true
}
}
)
NavigationBarItem(
icon = { Icon(Icons.Default.Person, contentDescription = "Profile") },
label = { Text(stringResource(R.string.menu_navigation_drawer_profile)) },
selected = currentDestination?.route == UserProfileScreenDestination.route,
onClick = {
controller.navigate(UserProfileScreenDestination) {
popUpTo(controller.graph.startDestinationId) {
inclusive = false
}
launchSingleTop = true
}
}
)
},
)
}
)
) { paddingValues ->
DestinationsNavHost(
navGraph = NavGraphs.root,
engine = engine,
navController = controller,
startRoute = startRoute ?: NavGraphs.root.startRoute,
modifier = Modifier.padding(paddingValues)
)
}
}

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
@@ -32,22 +33,17 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.atridad.mealient.R
import com.atridad.mealient.ui.AppTheme
import com.atridad.mealient.ui.Dimens
import com.atridad.mealient.ui.components.BaseScreenState
import com.atridad.mealient.ui.components.BaseScreenWithNavigation
import com.atridad.mealient.ui.components.TopProgressIndicator
import com.atridad.mealient.ui.components.previewBaseScreenState
import com.atridad.mealient.ui.preview.ColorSchemePreview
@Destination
@Composable
internal fun AddRecipeScreen(
baseScreenState: BaseScreenState,
viewModel: AddRecipeViewModel = hiltViewModel()
) {
val screenState by viewModel.screenState.collectAsState()
AddRecipeScreen(
baseScreenState = baseScreenState,
state = screenState,
onEvent = viewModel::onEvent,
)
@@ -55,7 +51,6 @@ internal fun AddRecipeScreen(
@Composable
internal fun AddRecipeScreen(
baseScreenState: BaseScreenState,
state: AddRecipeScreenState,
onEvent: (AddRecipeScreenEvent) -> Unit,
) {
@@ -74,19 +69,14 @@ internal fun AddRecipeScreen(
snackbarHostState.currentSnackbarData?.dismiss()
}
BaseScreenWithNavigation(
baseScreenState = baseScreenState,
snackbarHostState = snackbarHostState,
) { modifier ->
TopProgressIndicator(
modifier = modifier,
isLoading = state.isLoading,
) {
AddRecipeScreenContent(
state = state,
onEvent = onEvent,
)
}
TopProgressIndicator(
modifier = Modifier.fillMaxSize(),
isLoading = state.isLoading,
) {
AddRecipeScreenContent(
state = state,
onEvent = onEvent,
)
}
}
@@ -304,7 +294,6 @@ private fun AddRecipeInputField(
private fun AddRecipeScreenPreview() {
AppTheme {
AddRecipeScreen(
baseScreenState = previewBaseScreenState(),
state = AddRecipeScreenState(),
onEvent = {},
)

View File

@@ -30,18 +30,13 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.atridad.mealient.R
import com.atridad.mealient.ui.AppTheme
import com.atridad.mealient.ui.Dimens
import com.atridad.mealient.ui.components.BaseScreen
import com.atridad.mealient.ui.components.BaseScreenState
import com.atridad.mealient.ui.components.BaseScreenWithNavigation
import com.atridad.mealient.ui.components.TopProgressIndicator
import com.atridad.mealient.ui.components.previewBaseScreenState
import com.atridad.mealient.ui.preview.ColorSchemePreview
@Destination
@Composable
internal fun BaseURLScreen(
navController: NavController,
baseScreenState: BaseScreenState,
viewModel: BaseURLViewModel = hiltViewModel(),
) {
val screenState by viewModel.screenState.collectAsState()
@@ -54,7 +49,6 @@ internal fun BaseURLScreen(
BaseURLScreen(
state = screenState,
baseScreenState = baseScreenState,
onEvent = viewModel::onEvent,
)
}
@@ -62,27 +56,13 @@ internal fun BaseURLScreen(
@Composable
private fun BaseURLScreen(
state: BaseURLScreenState,
baseScreenState: BaseScreenState,
onEvent: (BaseURLScreenEvent) -> Unit,
) {
val content: @Composable (Modifier) -> Unit = {
BaseURLScreen(
modifier = it,
state = state,
onEvent = onEvent,
)
}
if (state.isNavigationEnabled) {
BaseScreenWithNavigation(
baseScreenState = baseScreenState,
content = content,
)
} else {
BaseScreen(
content = content,
)
}
BaseURLScreen(
modifier = Modifier.fillMaxSize(),
state = state,
onEvent = onEvent,
)
}
@Composable
@@ -177,29 +157,7 @@ private fun UrlInputField(
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = {
defaultKeyboardAction(ImeAction.Done)
onEvent(BaseURLScreenEvent.OnProceedClick)
},
)
)
}
@ColorSchemePreview
@Composable
private fun BaseURLScreenPreview() {
AppTheme {
BaseURLScreen(
state = BaseURLScreenState(
userInput = "https://www.google.com",
errorText = null,
isButtonEnabled = true,
isLoading = true,
isNavigationEnabled = false,
),
baseScreenState = previewBaseScreenState(),
onEvent = {},
)
}
}

View File

@@ -4,14 +4,25 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.atridad.mealient.ui.AppTheme
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.atridad.mealient.ui.theme.MealientTheme
import com.atridad.mealient.ui.Dimens
import com.atridad.mealient.ui.components.BaseScreen
import com.atridad.mealient.ui.preview.ColorSchemePreview
@@ -25,11 +36,19 @@ data class RecipeScreenArgs(
)
@Composable
internal fun RecipeScreen(
navigator: DestinationsNavigator,
viewModel: RecipeInfoViewModel = hiltViewModel(),
) {
val state by viewModel.uiState.collectAsState()
BaseScreen { modifier ->
BaseScreen(
topAppBar = {
RecipeTopAppBar(
title = state.title ?: "Recipe",
onNavigateBack = { navigator.navigateUp() }
)
}
) { modifier ->
RecipeScreen(
modifier = modifier,
state = state,
@@ -37,6 +56,39 @@ internal fun RecipeScreen(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RecipeTopAppBar(
title: String,
onNavigateBack: () -> Unit,
) {
TopAppBar(
title = {
Text(
text = title,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Navigate back",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
titleContentColor = MaterialTheme.colorScheme.onSurface,
navigationIconContentColor = MaterialTheme.colorScheme.onSurface
)
)
}
@Composable
private fun RecipeScreen(
state: RecipeInfoUiState,
@@ -74,7 +126,7 @@ private fun RecipeScreen(
@ColorSchemePreview
@Composable
private fun RecipeScreenPreview() {
AppTheme {
MealientTheme {
RecipeScreen(
state = RecipeInfoUiState(
showIngredients = true,

View File

@@ -3,9 +3,11 @@ package com.atridad.mealient.ui.recipes.list
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.atridad.mealient.R
@Composable
@@ -23,9 +25,13 @@ internal fun ConfirmDeleteDialog(
onClick = {
onConfirm(item)
},
colors = ButtonDefaults.textButtonColors(
contentColor = androidx.compose.material3.MaterialTheme.colorScheme.error
)
) {
Text(
text = stringResource(id = R.string.fragment_recipes_delete_recipe_confirm_dialog_positive_btn),
style = androidx.compose.material3.MaterialTheme.typography.labelLarge
)
}
},
@@ -35,12 +41,15 @@ internal fun ConfirmDeleteDialog(
) {
Text(
text = stringResource(id = R.string.fragment_recipes_delete_recipe_confirm_dialog_negative_btn),
style = androidx.compose.material3.MaterialTheme.typography.labelLarge
)
}
},
title = {
Text(
text = stringResource(id = R.string.fragment_recipes_delete_recipe_confirm_dialog_title),
style = androidx.compose.material3.MaterialTheme.typography.headlineSmall,
color = androidx.compose.material3.MaterialTheme.colorScheme.onSurface
)
},
text = {
@@ -49,7 +58,13 @@ internal fun ConfirmDeleteDialog(
id = R.string.fragment_recipes_delete_recipe_confirm_dialog_message,
item.entity.name
),
style = androidx.compose.material3.MaterialTheme.typography.bodyMedium,
color = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant
)
},
containerColor = androidx.compose.material3.MaterialTheme.colorScheme.surface,
titleContentColor = androidx.compose.material3.MaterialTheme.colorScheme.onSurface,
textContentColor = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant,
shape = androidx.compose.foundation.shape.RoundedCornerShape(28.dp)
)
}

View File

@@ -26,8 +26,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import coil.compose.AsyncImage
import com.atridad.mealient.R
import com.atridad.mealient.ui.AppTheme
import com.atridad.mealient.ui.Dimens
import com.atridad.mealient.ui.theme.MealientTheme
import com.atridad.mealient.ui.theme.Spacing
import com.atridad.mealient.ui.theme.BorderRadius
import com.atridad.mealient.ui.theme.ComponentSizing
import com.atridad.mealient.ui.preview.ColorSchemePreview
import com.atridad.mealient.ui.recipes.info.SUMMARY_ENTITY
import kotlin.random.Random
@@ -42,15 +44,24 @@ internal fun RecipeItem(
) {
Card(
modifier = modifier,
shape = RoundedCornerShape(BorderRadius.md),
colors = androidx.compose.material3.CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface,
),
elevation = androidx.compose.material3.CardDefaults.cardElevation(
defaultElevation = ComponentSizing.cardElevation,
pressedElevation = ComponentSizing.cardElevationPressed,
focusedElevation = ComponentSizing.cardElevationHovered,
),
) {
Column(
modifier = Modifier
.clickable(onClick = onItemClick)
.padding(
horizontal = Dimens.Medium,
vertical = Dimens.Small,
horizontal = Spacing.md,
vertical = Spacing.sm,
),
verticalArrangement = Arrangement.spacedBy(Dimens.Small),
verticalArrangement = Arrangement.spacedBy(Spacing.sm),
) {
RecipeHeader(
onDeleteClick = onDeleteClick,
@@ -66,7 +77,9 @@ internal fun RecipeItem(
text = recipe.entity.name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.headlineSmall,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(top = Spacing.xs)
)
}
}
@@ -82,7 +95,7 @@ private fun RecipeImage(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(2f) // 2:1
.clip(RoundedCornerShape(Dimens.Intermediate)),
.clip(RoundedCornerShape(BorderRadius.md)),
model = recipe.imageUrl,
contentDescription = stringResource(id = R.string.content_description_fragment_recipe_info_image),
placeholder = imageFallback,
@@ -105,6 +118,10 @@ private fun RecipeHeader(
) {
IconButton(
onClick = onDeleteClick,
colors = androidx.compose.material3.IconButtonDefaults.iconButtonColors(
containerColor = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer
)
) {
Icon(
imageVector = Icons.Default.Delete,
@@ -115,6 +132,18 @@ private fun RecipeHeader(
if (recipe.showFavoriteIcon) {
IconButton(
onClick = onFavoriteClick,
colors = androidx.compose.material3.IconButtonDefaults.iconButtonColors(
containerColor = if (recipe.entity.isFavorite) {
MaterialTheme.colorScheme.tertiaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant
},
contentColor = if (recipe.entity.isFavorite) {
MaterialTheme.colorScheme.onTertiaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
)
) {
Icon(
imageVector = if (recipe.entity.isFavorite) {
@@ -137,7 +166,7 @@ private fun RecipeHeader(
@Composable
private fun RecipeItemPreview() {
val isFavorite = Random.nextBoolean()
AppTheme {
MealientTheme {
RecipeItem(
recipe = RecipeListItemState(null, isFavorite, SUMMARY_ENTITY),
onDeleteClick = {},

View File

@@ -1,18 +1,18 @@
package com.atridad.mealient.ui.recipes.list
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -22,8 +22,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.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.paging.LoadState
@@ -34,22 +34,28 @@ import androidx.paging.compose.itemKey
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.navigate
import com.atridad.mealient.R
import com.atridad.mealient.ui.AppTheme
import com.atridad.mealient.ui.Dimens
import com.atridad.mealient.ui.components.BaseScreenState
import com.atridad.mealient.ui.components.BaseScreenWithNavigation
import com.atridad.mealient.ui.theme.Spacing
import com.atridad.mealient.ui.theme.BorderRadius
import com.atridad.mealient.ui.components.BaseScreen
import com.atridad.mealient.ui.components.CenteredProgressIndicator
import com.atridad.mealient.ui.components.LazyPagingColumnPullRefresh
import com.atridad.mealient.ui.components.OpenDrawerIconButton
import com.atridad.mealient.ui.destinations.RecipeScreenDestination
import com.atridad.mealient.ui.preview.ColorSchemePreview
import com.atridad.mealient.ui.destinations.AddRecipeScreenDestination
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Add
import com.atridad.mealient.ui.recipes.list.RecipeListItemState
import com.atridad.mealient.ui.recipes.list.RecipeListEvent
import com.atridad.mealient.ui.recipes.list.RecipeListState
import com.atridad.mealient.ui.recipes.list.ConfirmDeleteDialog
import com.atridad.mealient.ui.recipes.list.RecipeItem
import com.atridad.mealient.ui.recipes.list.RecipeListSnackbar
@Destination
@Composable
internal fun RecipesList(
navController: NavController,
baseScreenState: BaseScreenState,
viewModel: RecipesListViewModel = hiltViewModel(),
) {
val state = viewModel.screenState.collectAsState()
@@ -64,76 +70,140 @@ internal fun RecipesList(
RecipesList(
state = stateValue,
baseScreenState = baseScreenState,
onEvent = viewModel::onEvent,
navController = navController,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RecipesList(
state: RecipeListState,
baseScreenState: BaseScreenState,
onEvent: (RecipeListEvent) -> Unit,
navController: NavController,
) {
val recipes: LazyPagingItems<RecipeListItemState> =
state.pagingDataRecipeState.collectAsLazyPagingItems()
val isRefreshing = recipes.loadState.refresh is LoadState.Loading
var itemToDelete: RecipeListItemState? by remember { mutableStateOf(null) }
val snackbarHostState = remember { SnackbarHostState() }
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
BaseScreenWithNavigation(
baseScreenState = baseScreenState,
drawerState = drawerState,
topAppBar = {
RecipesTopAppBar(
searchQuery = state.searchQuery,
onValueChanged = { onEvent(RecipeListEvent.SearchQueryChanged(it)) },
drawerState = drawerState,
)
},
state.snackbarState?.let { snackbar ->
val message = snackbar.message
LaunchedEffect(message) {
snackbarHostState.showSnackbar(message)
onEvent(RecipeListEvent.SnackbarShown)
}
} ?: run {
snackbarHostState.currentSnackbarData?.dismiss()
}
itemToDelete?.let { item ->
ConfirmDeleteDialog(
onDismissRequest = { itemToDelete = null },
onConfirm = {
onEvent(RecipeListEvent.DeleteConfirmed(item))
itemToDelete = null
},
item = item,
)
}
BaseScreen(
snackbarHostState = snackbarHostState,
) { modifier ->
state.snackbarState?.message?.let { message ->
LaunchedEffect(message) {
snackbarHostState.showSnackbar(message)
onEvent(RecipeListEvent.SnackbarShown)
}
} ?: run {
snackbarHostState.currentSnackbarData?.dismiss()
}
itemToDelete?.let { item ->
ConfirmDeleteDialog(
onDismissRequest = { itemToDelete = null },
onConfirm = {
onEvent(RecipeListEvent.DeleteConfirmed(item))
itemToDelete = null
},
item = item,
)
}
when {
recipes.itemCount != 0 -> {
RecipesListData(
modifier = modifier,
recipes = recipes,
onDeleteClick = { itemToDelete = it },
onFavoriteClick = { onEvent(RecipeListEvent.FavoriteClick(it)) },
onItemClick = { onEvent(RecipeListEvent.RecipeClick(it)) },
Box(modifier = modifier.fillMaxSize()) {
Column(
modifier = Modifier.fillMaxSize()
) {
// Clean, spacious top app bar
androidx.compose.material3.TopAppBar(
title = {
androidx.compose.material3.Text(
text = stringResource(R.string.menu_navigation_drawer_recipes_list),
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onSurface
)
},
colors = androidx.compose.material3.TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
titleContentColor = MaterialTheme.colorScheme.onSurface
)
)
}
isRefreshing -> {
CenteredProgressIndicator(
modifier = modifier
// Full-width search bar with proper Material 3 spacing
androidx.compose.material3.OutlinedTextField(
value = state.searchQuery,
onValueChange = { onEvent(RecipeListEvent.SearchQueryChanged(it)) },
placeholder = {
androidx.compose.material3.Text(
text = stringResource(R.string.search_recipes_hint),
style = MaterialTheme.typography.bodyLarge
)
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = Spacing.lg, vertical = Spacing.md),
textStyle = MaterialTheme.typography.bodyLarge,
colors = androidx.compose.material3.OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline,
focusedLabelColor = MaterialTheme.colorScheme.primary,
unfocusedLabelColor = MaterialTheme.colorScheme.onSurfaceVariant
),
shape = RoundedCornerShape(BorderRadius.lg),
singleLine = true,
leadingIcon = {
androidx.compose.material3.Icon(
imageVector = Icons.Default.Search,
contentDescription = "Search",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
)
// Recipe list content
when {
recipes.itemCount != 0 -> {
RecipesListData(
modifier = Modifier.weight(1f),
recipes = recipes,
onDeleteClick = { itemToDelete = it },
onFavoriteClick = { onEvent(RecipeListEvent.FavoriteClick(it)) },
onItemClick = { onEvent(RecipeListEvent.RecipeClick(it)) },
)
}
isRefreshing -> {
CenteredProgressIndicator(
modifier = Modifier.weight(1f)
)
}
else -> {
RecipesListError(
modifier = Modifier.weight(1f),
recipes = recipes,
)
}
}
}
else -> {
RecipesListError(
modifier = modifier,
recipes = recipes,
// FAB for adding recipes
androidx.compose.material3.FloatingActionButton(
onClick = {
navController.navigate(AddRecipeScreenDestination) {
launchSingleTop = true
}
},
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(Spacing.lg),
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
) {
androidx.compose.material3.Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add recipe",
modifier = Modifier.size(24.dp)
)
}
}
@@ -172,8 +242,8 @@ private fun RecipesListData(
modifier = modifier
.fillMaxSize(),
lazyPagingItems = recipes,
verticalArrangement = Arrangement.spacedBy(Dimens.Medium),
contentPadding = PaddingValues(Dimens.Medium),
verticalArrangement = Arrangement.spacedBy(Spacing.md),
contentPadding = PaddingValues(Spacing.md),
) {
items(
count = recipes.itemCount,
@@ -195,45 +265,3 @@ private fun RecipesListData(
}
}
@Composable
internal fun RecipesTopAppBar(
searchQuery: String,
onValueChanged: (String) -> Unit,
drawerState: DrawerState,
) {
Row(
modifier = Modifier
.padding(
horizontal = Dimens.Medium,
vertical = Dimens.Small,
)
.clip(RoundedCornerShape(Dimens.Medium))
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(end = Dimens.Medium),
verticalAlignment = Alignment.CenterVertically,
) {
OpenDrawerIconButton(
drawerState = drawerState,
)
SearchTextField(
modifier = Modifier
.weight(1f),
searchQuery = searchQuery,
onValueChanged = onValueChanged,
placeholder = R.string.search_recipes_hint,
)
}
}
@ColorSchemePreview
@Composable
private fun RecipesTopAppBarPreview() {
AppTheme {
RecipesTopAppBar(
searchQuery = "",
onValueChanged = {},
drawerState = rememberDrawerState(initialValue = DrawerValue.Closed),
)
}
}

View File

@@ -141,6 +141,8 @@ constructor(
_screenState.update { it.copy(searchQuery = event.query) }
recipeRepo.updateNameQuery(event.query)
}
}
internal data class RecipeListState(
@@ -163,4 +165,6 @@ internal sealed interface RecipeListEvent {
data object SnackbarShown : RecipeListEvent
data class SearchQueryChanged(val query: String) : RecipeListEvent
}

View File

@@ -16,8 +16,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import com.atridad.mealient.R
import com.atridad.mealient.ui.AppTheme
import com.atridad.mealient.ui.theme.MealientTheme
import com.atridad.mealient.ui.preview.ColorSchemePreview
@Composable
@@ -27,7 +28,7 @@ internal fun SearchTextField(
@StringRes placeholder: Int,
modifier: Modifier = Modifier,
) {
TextField(
androidx.compose.material3.OutlinedTextField(
modifier = modifier
.semantics { testTag = "search-recipes-field" },
value = searchQuery,
@@ -35,12 +36,15 @@ internal fun SearchTextField(
placeholder = {
Text(
text = stringResource(id = placeholder),
style = androidx.compose.material3.MaterialTheme.typography.bodyMedium,
color = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant
)
},
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = null,
contentDescription = stringResource(R.string.search_recipes_hint),
tint = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant
)
},
keyboardOptions = KeyboardOptions(
@@ -50,23 +54,24 @@ internal fun SearchTextField(
onSearch = { defaultKeyboardAction(ImeAction.Done) }
),
singleLine = true,
colors = TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
errorContainerColor = Color.Transparent
)
textStyle = androidx.compose.material3.MaterialTheme.typography.bodyLarge,
colors = androidx.compose.material3.OutlinedTextFieldDefaults.colors(
focusedBorderColor = androidx.compose.material3.MaterialTheme.colorScheme.primary,
unfocusedBorderColor = androidx.compose.material3.MaterialTheme.colorScheme.outline,
focusedLabelColor = androidx.compose.material3.MaterialTheme.colorScheme.primary,
unfocusedLabelColor = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant,
focusedTextColor = androidx.compose.material3.MaterialTheme.colorScheme.onSurface,
unfocusedTextColor = androidx.compose.material3.MaterialTheme.colorScheme.onSurface,
cursorColor = androidx.compose.material3.MaterialTheme.colorScheme.primary
),
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
)
}
@ColorSchemePreview
@Composable
private fun SearchTextFieldPreview() {
AppTheme {
MealientTheme {
SearchTextField(
searchQuery = "",
onValueChanged = {},

View File

@@ -0,0 +1,201 @@
package com.atridad.mealient.ui.settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.Smartphone
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.atridad.mealient.ui.theme.Spacing
import androidx.compose.foundation.clickable
import androidx.compose.ui.graphics.vector.ImageVector
@Destination
@Composable
internal fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(),
) {
val state by viewModel.screenState.collectAsState()
SettingsScreen(
state = state,
onEvent = viewModel::onEvent,
)
}
@Composable
private fun SettingsScreen(
state: SettingsScreenState,
onEvent: (SettingsScreenEvent) -> Unit,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(Spacing.lg),
verticalArrangement = Arrangement.spacedBy(Spacing.lg)
) {
// Header
Text(
text = "Settings",
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onSurface
)
// Theme Selection Card
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(
defaultElevation = 2.dp
)
) {
Column(
modifier = Modifier.padding(Spacing.lg)
) {
Text(
text = "Theme",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.size(Spacing.md))
// Theme options
ThemeOption(
title = "Device",
subtitle = "Follow system theme",
icon = Icons.Default.Smartphone,
isSelected = state.themeMode == ThemeMode.DEVICE,
onClick = { onEvent(SettingsScreenEvent.ThemeModeChanged(ThemeMode.DEVICE)) }
)
androidx.compose.material3.HorizontalDivider(modifier = Modifier.padding(vertical = Spacing.sm))
ThemeOption(
title = "Light",
subtitle = "Always use light theme",
icon = Icons.Default.LightMode,
isSelected = state.themeMode == ThemeMode.LIGHT,
onClick = { onEvent(SettingsScreenEvent.ThemeModeChanged(ThemeMode.LIGHT)) }
)
androidx.compose.material3.HorizontalDivider(modifier = Modifier.padding(vertical = Spacing.sm))
ThemeOption(
title = "Dark",
subtitle = "Always use dark theme",
icon = Icons.Default.DarkMode,
isSelected = state.themeMode == ThemeMode.DARK,
onClick = { onEvent(SettingsScreenEvent.ThemeModeChanged(ThemeMode.DARK)) }
)
}
}
// Logout Card
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
elevation = CardDefaults.cardElevation(
defaultElevation = 2.dp
)
) {
ListItem(
headlineContent = {
Text(
text = "Logout",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onErrorContainer
)
},
supportingContent = {
Text(
text = "Sign out of your account",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer
)
},
leadingContent = {
Icon(
imageVector = Icons.AutoMirrored.Filled.Logout,
contentDescription = "Logout",
tint = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.size(24.dp)
)
},
modifier = Modifier.clickable { onEvent(SettingsScreenEvent.Logout) }
)
}
}
}
@Composable
private fun ThemeOption(
title: String,
subtitle: String,
icon: ImageVector,
isSelected: Boolean,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(vertical = Spacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.size(Spacing.md))
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
RadioButton(
selected = isSelected,
onClick = onClick
)
}
}

View File

@@ -0,0 +1,6 @@
package com.atridad.mealient.ui.settings
internal sealed interface SettingsScreenEvent {
data class ThemeModeChanged(val themeMode: ThemeMode) : SettingsScreenEvent
data object Logout : SettingsScreenEvent
}

View File

@@ -0,0 +1,12 @@
package com.atridad.mealient.ui.settings
data class SettingsScreenState(
val themeMode: ThemeMode = ThemeMode.DEVICE,
val isLoading: Boolean = false,
)
enum class ThemeMode {
DEVICE,
LIGHT,
DARK,
}

View File

@@ -0,0 +1,48 @@
package com.atridad.mealient.ui.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.atridad.mealient.data.storage.PreferencesStorage
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
internal class SettingsViewModel @Inject constructor(
private val prefs: PreferencesStorage,
) : ViewModel() {
private val _screenState = MutableStateFlow(SettingsScreenState())
val screenState: StateFlow<SettingsScreenState> = _screenState.asStateFlow()
init {
viewModelScope.launch {
val stored = prefs.getValue(prefs.themeModeKey)
val mode = stored?.let { runCatching { ThemeMode.valueOf(it) }.getOrNull() }
if (mode != null) {
_screenState.value = _screenState.value.copy(themeMode = mode)
}
}
}
fun onEvent(event: SettingsScreenEvent) {
when (event) {
is SettingsScreenEvent.ThemeModeChanged -> {
_screenState.value = _screenState.value.copy(
themeMode = event.themeMode
)
viewModelScope.launch {
prefs.storeValues(Pair(prefs.themeModeKey, event.themeMode.name))
}
}
is SettingsScreenEvent.Logout -> {
_screenState.value = _screenState.value.copy(
isLoading = true
)
}
}
}
}

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="fragment_disclaimer_button_okay_timer">
<item quantity="one">Okay (%d Sekunde)</item>
<item quantity="other">Okay (%d Sekunden)</item>
</plurals>
</resources>

View File

@@ -1,83 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fragment_authentication_input_hint_email">E-Mail oder Nutzername</string>
<string name="fragment_authentication_input_hint_password">Passwort</string>
<string name="fragment_authentication_input_hint_url">Server-URL</string>
<string name="fragment_authentication_button_login">Anmeldung</string>
<string name="content_description_view_holder_recipe_image">Bild der gekochten Mahlzeit</string>
<string name="menu_navigation_drawer_logout">Abmeldung</string>
<string name="view_holder_recipe_text_placeholder">Laden…</string>
<string name="fragment_recipe_info_ingredients_header">Inhaltsstoffe</string>
<string name="fragment_recipe_info_instructions_header">Anweisungen</string>
<string name="fragment_disclaimer_main_text">Dieses Projekt wird unabhängig vom Mealie-Kernprojekt entwickelt. Es ist NICHT mit den Mealie-Kernentwicklern verbunden. Alle Probleme müssen an das Mealient-Repository und NICHT an das Mealie-Repository gemeldet werden.</string>
<string name="fragment_baseurl_url_input_empty">URL darf nicht leer sein</string>
<string name="fragment_base_url_no_connection">Kann keine Verbindung herstellen, Adresse prüfen.</string>
<string name="fragment_base_url_unexpected_response">Unerwartete Antwort. Ist es Mealie?</string>
<string name="fragment_base_url_malformed_url">URL-Format prüfen: %s</string>
<string name="fragment_base_url_save">Weiter</string>
<string name="fragment_base_url_invalid_certificate_title">Die Identität des Servers konnte nicht überprüft werden</string>
<string name="fragment_base_url_invalid_certificate_message">Vertrauen Sie diesem Zertifikat?\n\nInformationen zum Zertifikat:\nAussteller: %1$s\nBetreff: %2$s\nGültig von: %3$s\nGültig bis: %4$s</string>
<string name="fragment_base_url_invalid_certificate_accept">Vertrauen</string>
<string name="fragment_base_url_invalid_certificate_deny">Nein</string>
<string name="menu_navigation_drawer_login">Anmeldung</string>
<string name="fragment_disclaimer_button_okay">Okay</string>
<string name="view_holder_recipe_instructions_step">Schritt: %d</string>
<string name="fragment_authentication_email_input_empty">E-Mail kann nicht leer sein</string>
<string name="fragment_authentication_password_input_empty">Das Passwort darf nicht leer sein</string>
<string name="fragment_authentication_credentials_incorrect">E-Mail oder Passwort sind falsch.</string>
<string name="fragment_authentication_unknown_error">Es ist ein Fehler aufgetreten, bitte versuchen Sie es erneut.</string>
<string name="fragment_add_recipe_recipe_name">Name des Rezepts</string>
<string name="fragment_add_recipe_recipe_description">Beschreibung</string>
<string name="menu_navigation_drawer_add_recipe">Rezept hinzufügen</string>
<string name="menu_navigation_drawer_recipes_list">Rezepte</string>
<string name="fragment_add_recipe_recipe_yield">Ausbeute des Rezepts</string>
<string name="fragment_add_recipe_save_button">Rezept speichern</string>
<string name="fragment_add_recipe_new_instruction">Neuer Schritt</string>
<string name="fragment_add_recipe_new_ingredient">Neue Zutat</string>
<string name="fragment_add_recipe_public_recipe">Öffentliches Rezept</string>
<string name="fragment_add_recipe_disable_comments">Kommentare deaktivieren</string>
<string name="fragment_add_recipe_ingredient_hint">Zutat</string>
<string name="fragment_add_recipe_instruction_hint">Beschreibung der Schritte</string>
<string name="fragment_add_recipe_name_error">Rezeptname darf nicht leer sein</string>
<string name="fragment_add_recipe_save_error">Etwas ist schief gelaufen</string>
<string name="fragment_add_recipe_save_success">Rezept erfolgreich gespeichert</string>
<string name="fragment_add_recipe_clear_button">Klar</string>
<string name="fragment_base_url_url_input_helper_text">Beispiel: demo.mealie.io</string>
<string name="fragment_authentication_email_input_helper_text">Beispiel: changeme@example.com</string>
<string name="fragment_authentication_password_input_helper_text">Beispiel: MyPassword</string>
<string name="fragment_recipes_last_page_loaded_toast">Zuletzt geladene Seite</string>
<string name="fragment_recipes_load_failure_toast" comment="EXAMPLE: Load error: unauthorized.">Ladefehler: %1$s.</string>
<string name="fragment_recipes_load_failure_toast_no_reason">Laden fehlgeschlagen.</string>
<string name="fragment_recipes_load_failure_toast_unauthorized">unbefugt</string>
<string name="fragment_recipes_load_failure_toast_unexpected_response">unerwartete Antwort</string>
<string name="fragment_recipes_load_failure_toast_no_connection">keine Verbindung</string>
<string name="fragment_recipes_favorite_update_failed">Favoritenstatusaktualisierung fehlgeschlagen</string>
<string name="fragment_recipes_delete_recipe_failed">Rezeptentfernung fehlgeschlagen</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_title">Rezept löschen</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_message">Sind Sie sicher, dass Sie %1$slöschen möchten? Dies kann nicht rückgängig gemacht werden.</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_positive_btn">Bestätigen Sie</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_negative_btn">Abbrechen</string>
<string name="menu_navigation_drawer_change_url">URL ändern</string>
<string name="search_recipes_hint">Rezepte suchen</string>
<string name="view_toolbar_navigation_icon_content_description">Navigationsschublade öffnen</string>
<string name="fragment_recipes_list_no_recipes">Keine Rezepte</string>
<string name="activity_share_recipe_success_toast">Rezept erfolgreich gespeichert.</string>
<string name="activity_share_recipe_failure_toast">Etwas ist schief gelaufen.</string>
<string name="content_description_activity_share_recipe_progress">Indikator für den Fortschritt</string>
<string name="view_holder_recipe_favorite_content_description">Artikel ist Favorit</string>
<string name="view_holder_recipe_non_favorite_content_description">Artikel ist nicht beliebt</string>
<string name="view_holder_recipe_delete_content_description">Rezept löschen</string>
<string name="fragment_recipes_favorite_added">%1$s zu den Favoriten hinzugefügt</string>
<string name="fragment_recipes_favorite_removed">%1$s aus den Favoriten entfernt</string>
<string name="menu_navigation_drawer_shopping_lists">Einkaufslisten</string>
<string name="menu_navigation_drawer_email_logs">E-Mail Protokolle</string>
<string name="activity_main_email_logs_subject">Mealient Protokolle</string>
<string name="activity_main_email_logs_confirmation_message">Die Protokolle enthalten sensible Daten, wie zum Beispiel API-Token, Einkaufslisten und Rezepte. API-Token können per Web-Client widerrufen werden. Die Datei kann angesehen und bearbeitet werden, wenn Sie sie stattdessen an sich selbst senden.</string>
<string name="activity_main_email_logs_confirmation_title">Sende sensible Daten</string>
<string name="activity_main_email_logs_confirmation_positive">Wählen Sie eine Sendemethode</string>
<string name="activity_main_email_logs_confirmation_negative">Abbrechen</string>
<string name="activity_main_logout_confirmation_title">Abmelden läuft</string>
<string name="activity_main_logout_confirmation_message">Sind Sie sicher, dass Sie sich abmelden möchten?</string>
<string name="activity_main_logout_confirmation_positive">Abmelden</string>
<string name="activity_main_logout_confirmation_negative">Abbrechen</string>
</resources>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="fragment_disclaimer_button_okay_timer">
<item quantity="one">Bien, (%d segundo)</item>
<item quantity="other">Bien, (%d segundos)</item>
</plurals>
</resources>

View File

@@ -1,83 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fragment_authentication_input_hint_email">Email o nombre de usuario</string>
<string name="fragment_authentication_input_hint_password">Contraseña</string>
<string name="fragment_authentication_input_hint_url">URL del servidor</string>
<string name="fragment_authentication_button_login">Iniciar sesión</string>
<string name="content_description_view_holder_recipe_image">Foto de la comida cocinada</string>
<string name="menu_navigation_drawer_logout">Cerrar sesión</string>
<string name="view_holder_recipe_text_placeholder">Cargando…</string>
<string name="fragment_recipe_info_ingredients_header">Ingredientes</string>
<string name="fragment_recipe_info_instructions_header">Instrucciones</string>
<string name="fragment_disclaimer_main_text">Este proyecto se desarrolla independientemente del proyecto Mealie. NO está asociado con los desarrolladores de Mealie. Cualquier problema debe ser reportado al repositorio de Mealient, NO al repositorio de Mealie.</string>
<string name="fragment_baseurl_url_input_empty">La URL no puede estar vacía</string>
<string name="fragment_base_url_no_connection">No se puede conectar, verifique la dirección.</string>
<string name="fragment_base_url_unexpected_response">Respuesta inesperada. ¿Es Mealie?</string>
<string name="fragment_base_url_malformed_url">Comprobar el formato de URL: %s</string>
<string name="fragment_base_url_save">Continuar</string>
<string name="fragment_base_url_invalid_certificate_title">No se ha podido verificar la identidad del servidor</string>
<string name="fragment_base_url_invalid_certificate_message">¿Confía en este certificado?\n\nInformación del certificado:\nEmisor: %1$s\nAsunto: %2$s\nVálido Desde: %3$s\nVálido hasta: %4$s</string>
<string name="fragment_base_url_invalid_certificate_accept">Confíe en</string>
<string name="fragment_base_url_invalid_certificate_deny">No</string>
<string name="menu_navigation_drawer_login">Iniciar sesión</string>
<string name="fragment_disclaimer_button_okay">Aceptar</string>
<string name="view_holder_recipe_instructions_step">Paso: %d</string>
<string name="fragment_authentication_email_input_empty">El correo electrónico no puede estar vacío</string>
<string name="fragment_authentication_password_input_empty">La contraseña no puede estar vacía</string>
<string name="fragment_authentication_credentials_incorrect">Correo electrónico o contraseña incorrectos.</string>
<string name="fragment_authentication_unknown_error">Algo salió mal, por favor vuelve a intentarlo.</string>
<string name="fragment_add_recipe_recipe_name">Nombre de la receta</string>
<string name="fragment_add_recipe_recipe_description">Descripción</string>
<string name="menu_navigation_drawer_add_recipe">Agregar receta</string>
<string name="menu_navigation_drawer_recipes_list">Recetas</string>
<string name="fragment_add_recipe_recipe_yield">Porciones</string>
<string name="fragment_add_recipe_save_button">Guardar receta</string>
<string name="fragment_add_recipe_new_instruction">Nuevo paso</string>
<string name="fragment_add_recipe_new_ingredient">Nuevo ingrediente</string>
<string name="fragment_add_recipe_public_recipe">Receta pública</string>
<string name="fragment_add_recipe_disable_comments">Desactivar comentarios</string>
<string name="fragment_add_recipe_ingredient_hint">Ingrediente</string>
<string name="fragment_add_recipe_instruction_hint">Descripción del paso</string>
<string name="fragment_add_recipe_name_error">El nombre de la receta no puede estar vacío</string>
<string name="fragment_add_recipe_save_error">Algo salió mal</string>
<string name="fragment_add_recipe_save_success">Receta guardada con éxito</string>
<string name="fragment_add_recipe_clear_button">Limpiar</string>
<string name="fragment_base_url_url_input_helper_text">Ejemplo: demo.mealie.io</string>
<string name="fragment_authentication_email_input_helper_text">Ejemplo: changeme@example.com</string>
<string name="fragment_authentication_password_input_helper_text">Ejemplo: MyPassword</string>
<string name="fragment_recipes_last_page_loaded_toast">Última página cargada</string>
<string name="fragment_recipes_load_failure_toast" comment="EXAMPLE: Load error: unauthorized.">Error al cargar: %1$s.</string>
<string name="fragment_recipes_load_failure_toast_no_reason">La carga falló.</string>
<string name="fragment_recipes_load_failure_toast_unauthorized">no autorizado</string>
<string name="fragment_recipes_load_failure_toast_unexpected_response">respuesta inesperada</string>
<string name="fragment_recipes_load_failure_toast_no_connection">sin conexión</string>
<string name="fragment_recipes_favorite_update_failed">Error al actualizar el estado de favorito</string>
<string name="fragment_recipes_delete_recipe_failed">Error al eliminar la receta</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_title">Eliminar receta</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_message">¿Está seguro que desea eliminar %1$s? Esto no se puede deshacer.</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_positive_btn">Confirmar</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_negative_btn">Cancelar</string>
<string name="menu_navigation_drawer_change_url">Cambiar URL</string>
<string name="search_recipes_hint">Buscar recetas</string>
<string name="view_toolbar_navigation_icon_content_description">Abrir cajón de navegación</string>
<string name="fragment_recipes_list_no_recipes">Sin recetas</string>
<string name="activity_share_recipe_success_toast">Receta guardada exitosamente.</string>
<string name="activity_share_recipe_failure_toast">Algo salió mal.</string>
<string name="content_description_activity_share_recipe_progress">Indicador de progreso</string>
<string name="view_holder_recipe_favorite_content_description">El artículo es favorito</string>
<string name="view_holder_recipe_non_favorite_content_description">El artículo no es favorito</string>
<string name="view_holder_recipe_delete_content_description">Eliminar receta</string>
<string name="fragment_recipes_favorite_added">Añadido %1$s a favoritos</string>
<string name="fragment_recipes_favorite_removed">Eliminado %1$s de favoritos</string>
<string name="menu_navigation_drawer_shopping_lists">Listas de la compra</string>
<string name="menu_navigation_drawer_email_logs">Registros de correo electrónico</string>
<string name="activity_main_email_logs_subject">Registros mealientes</string>
<string name="activity_main_email_logs_confirmation_message">Los registros contienen datos sensibles como tokens de API, listas de la compra y recetas. Los tokens de API se pueden revocar mediante el cliente web. El archivo se puede ver y editar si te lo envías a ti mismo.</string>
<string name="activity_main_email_logs_confirmation_title">Envío de datos sensibles</string>
<string name="activity_main_email_logs_confirmation_positive">Elija cómo enviar</string>
<string name="activity_main_email_logs_confirmation_negative">Cancelar</string>
<string name="activity_main_logout_confirmation_title">Cerrar sesión</string>
<string name="activity_main_logout_confirmation_message">¿Seguro que quieres desconectarte?</string>
<string name="activity_main_logout_confirmation_positive">Cerrar sesión</string>
<string name="activity_main_logout_confirmation_negative">Cancelar</string>
</resources>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="fragment_disclaimer_button_okay_timer">
<item quantity="one">Ok (%d seconde)</item>
<item quantity="other">Ok (%d secondes)</item>
</plurals>
</resources>

View File

@@ -1,83 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fragment_authentication_input_hint_email">Email ou nom d\'utilisateur</string>
<string name="fragment_authentication_input_hint_password">Mot de passe</string>
<string name="fragment_authentication_input_hint_url">URL du serveur</string>
<string name="fragment_authentication_button_login">Connexion</string>
<string name="content_description_view_holder_recipe_image">Photo du repas cuisiné</string>
<string name="menu_navigation_drawer_logout">Déconnexion</string>
<string name="view_holder_recipe_text_placeholder">Chargement de…</string>
<string name="fragment_recipe_info_ingredients_header">Ingrédients</string>
<string name="fragment_recipe_info_instructions_header">Instructions</string>
<string name="fragment_disclaimer_main_text">Ce projet est développé indépendamment du projet principal Mealie. Il n\'est PAS associé aux développeurs de Mealie. Tout problème doit être signalé au dépôt Mealient, et NON au dépôt Mealie.</string>
<string name="fragment_baseurl_url_input_empty">L\'URL ne peut pas être vide</string>
<string name="fragment_base_url_no_connection">Impossible de se connecter, vérifier l\'adresse.</string>
<string name="fragment_base_url_unexpected_response">Réponse inattendue. Est-ce Mealie ?</string>
<string name="fragment_base_url_malformed_url">Vérifier le format de l\'URL : %s</string>
<string name="fragment_base_url_save">Procéder</string>
<string name="fragment_base_url_invalid_certificate_title">L\'identité du serveur n\'a pas pu être vérifiée</string>
<string name="fragment_base_url_invalid_certificate_message">Faites-vous confiance à ce certificat ?\n\nInformations sur le certificat :\nÉmetteur : %1$s\nSujet : %2$s\nValable du : %3$s\nValable jusqu\'au : %4$s</string>
<string name="fragment_base_url_invalid_certificate_accept">Confiance</string>
<string name="fragment_base_url_invalid_certificate_deny">Non</string>
<string name="menu_navigation_drawer_login">Connexion</string>
<string name="fragment_disclaimer_button_okay">D\'accord</string>
<string name="view_holder_recipe_instructions_step">Étape : %d</string>
<string name="fragment_authentication_email_input_empty">L\'e-mail ne peut pas être vide</string>
<string name="fragment_authentication_password_input_empty">Le mot de passe ne peut pas être vide</string>
<string name="fragment_authentication_credentials_incorrect">L\'e-mail ou le mot de passe est incorrect.</string>
<string name="fragment_authentication_unknown_error">Un problème s\'est produit, veuillez réessayer.</string>
<string name="fragment_add_recipe_recipe_name">Nom de la recette</string>
<string name="fragment_add_recipe_recipe_description">Description</string>
<string name="menu_navigation_drawer_add_recipe">Ajouter une recette</string>
<string name="menu_navigation_drawer_recipes_list">Recettes</string>
<string name="fragment_add_recipe_recipe_yield">Rendement de la recette</string>
<string name="fragment_add_recipe_save_button">Enregistrer la recette</string>
<string name="fragment_add_recipe_new_instruction">Nouvelle étape</string>
<string name="fragment_add_recipe_new_ingredient">Nouvel ingrédient</string>
<string name="fragment_add_recipe_public_recipe">Recette publique</string>
<string name="fragment_add_recipe_disable_comments">Désactiver les commentaires</string>
<string name="fragment_add_recipe_ingredient_hint">Ingrédient</string>
<string name="fragment_add_recipe_instruction_hint">Description des étapes</string>
<string name="fragment_add_recipe_name_error">Le nom de la recette ne peut pas être vide</string>
<string name="fragment_add_recipe_save_error">Quelque chose n\'a pas fonctionné</string>
<string name="fragment_add_recipe_save_success">Sauvegarde réussie de la recette</string>
<string name="fragment_add_recipe_clear_button">Clair</string>
<string name="fragment_base_url_url_input_helper_text">Exemple : demo.mealie.io</string>
<string name="fragment_authentication_email_input_helper_text">Exemple : changeme@example.com</string>
<string name="fragment_authentication_password_input_helper_text">Exemple : MyPassword</string>
<string name="fragment_recipes_last_page_loaded_toast">Dernière page chargée</string>
<string name="fragment_recipes_load_failure_toast" comment="EXAMPLE: Load error: unauthorized.">Erreur de chargement : %1$s.</string>
<string name="fragment_recipes_load_failure_toast_no_reason">Le chargement a échoué.</string>
<string name="fragment_recipes_load_failure_toast_unauthorized">non autorisé</string>
<string name="fragment_recipes_load_failure_toast_unexpected_response">réponse inattendue</string>
<string name="fragment_recipes_load_failure_toast_no_connection">pas de connexion</string>
<string name="fragment_recipes_favorite_update_failed">La mise à jour du statut de favori a échoué</string>
<string name="fragment_recipes_delete_recipe_failed">Échec de la suppression de la recette</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_title">Supprimer la recette</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_message">Êtes-vous sûr de vouloir supprimer %1$s? Cette opération ne peut être annulée.</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_positive_btn">Confirmer</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_negative_btn">Annuler</string>
<string name="menu_navigation_drawer_change_url">Modifier l\'URL</string>
<string name="search_recipes_hint">Rechercher des recettes</string>
<string name="view_toolbar_navigation_icon_content_description">Ouvrir le tiroir de navigation</string>
<string name="fragment_recipes_list_no_recipes">Pas de recettes</string>
<string name="activity_share_recipe_success_toast">La recette a été enregistrée avec succès.</string>
<string name="activity_share_recipe_failure_toast">Quelque chose n\'a pas fonctionné.</string>
<string name="content_description_activity_share_recipe_progress">Indicateur de progrès</string>
<string name="view_holder_recipe_favorite_content_description">L\'article est le préféré</string>
<string name="view_holder_recipe_non_favorite_content_description">L\'article n\'est pas favori</string>
<string name="view_holder_recipe_delete_content_description">Supprimer la recette</string>
<string name="fragment_recipes_favorite_added">Ajout de %1$s aux favoris</string>
<string name="fragment_recipes_favorite_removed">Suppression de %1$s des favoris</string>
<string name="menu_navigation_drawer_shopping_lists">Listes de courses</string>
<string name="menu_navigation_drawer_email_logs">Journaux des courriels</string>
<string name="activity_main_email_logs_subject">Journaux de la maltraitance</string>
<string name="activity_main_email_logs_confirmation_message">Les journaux contiennent des données sensibles telles que les jetons API, les listes d\'achats et les recettes. Les jetons d\'API peuvent être révoqués à l\'aide du client web. Le fichier peut être consulté et modifié si vous vous l\'envoyez à vous-même.</string>
<string name="activity_main_email_logs_confirmation_title">Envoi de données sensibles</string>
<string name="activity_main_email_logs_confirmation_positive">Choisir le mode d\'envoi</string>
<string name="activity_main_email_logs_confirmation_negative">Annuler</string>
<string name="activity_main_logout_confirmation_title">Déconnexion</string>
<string name="activity_main_logout_confirmation_message">Êtes-vous sûr de vouloir vous déconnecter ?</string>
<string name="activity_main_logout_confirmation_positive">Déconnexion</string>
<string name="activity_main_logout_confirmation_negative">Annuler</string>
</resources>

View File

@@ -1,9 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
<item name="postSplashScreenTheme">@style/AppTheme</item>
<item name="windowSplashScreenBackground">@android:color/black</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_splash_screen</item>
</style>
<style name="AppTheme" parent="Theme.Material3.DynamicColors.DayNight">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:enforceStatusBarContrast">false</item>
</style>
</resources>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="fragment_disclaimer_button_okay_timer">
<item quantity="one">Oké (%d seconde)</item>
<item quantity="other">Oké (%d seconden)</item>
</plurals>
</resources>

View File

@@ -1,83 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fragment_authentication_input_hint_email">E-mail of gebruikersnaam</string>
<string name="fragment_authentication_input_hint_password">Wachtwoord</string>
<string name="fragment_authentication_input_hint_url">Server URL</string>
<string name="fragment_authentication_button_login">Inloggen</string>
<string name="content_description_view_holder_recipe_image">Foto van de bereide maaltijd</string>
<string name="menu_navigation_drawer_logout">Afmelden</string>
<string name="view_holder_recipe_text_placeholder">Laden…</string>
<string name="fragment_recipe_info_ingredients_header">Ingrediënten</string>
<string name="fragment_recipe_info_instructions_header">Instructies</string>
<string name="fragment_disclaimer_main_text">Dit project wordt onafhankelijk van het Mealie-kernproject ontwikkeld. Het is NIET verbonden met de kernontwikkelaars van Mealie. Eventuele problemen moeten worden gerapporteerd aan de Mealient repository, NIET aan de Mealie repository.</string>
<string name="fragment_baseurl_url_input_empty">URL kan niet leeg zijn</string>
<string name="fragment_base_url_no_connection">Kan geen verbinding maken, controleer adres.</string>
<string name="fragment_base_url_unexpected_response">Onverwachte reactie. Is het Mealie?</string>
<string name="fragment_base_url_malformed_url">Controleer URL-indeling: %s</string>
<string name="fragment_base_url_save">Ga verder</string>
<string name="fragment_base_url_invalid_certificate_title">De identiteit van de server kon niet worden geverifieerd</string>
<string name="fragment_base_url_invalid_certificate_message">Vertrouwt u dit certificaat?\n\nCertificaatinformatie:\nUitgevende instelling: %1$s\nOnderwerp: %2$s\nGeldig vanaf: %3$s\nGeldig tot: %4$s</string>
<string name="fragment_base_url_invalid_certificate_accept">Vertrouwen</string>
<string name="fragment_base_url_invalid_certificate_deny">Geen</string>
<string name="menu_navigation_drawer_login">Inloggen</string>
<string name="fragment_disclaimer_button_okay">Oké</string>
<string name="view_holder_recipe_instructions_step">Stap: %d</string>
<string name="fragment_authentication_email_input_empty">E-mail kan niet leeg zijn</string>
<string name="fragment_authentication_password_input_empty">Wachtwoord kan niet leeg zijn</string>
<string name="fragment_authentication_credentials_incorrect">E-mail of wachtwoord is onjuist.</string>
<string name="fragment_authentication_unknown_error">Er is iets misgegaan, probeer het opnieuw.</string>
<string name="fragment_add_recipe_recipe_name">Naam recept</string>
<string name="fragment_add_recipe_recipe_description">Beschrijving</string>
<string name="menu_navigation_drawer_add_recipe">Recept toevoegen</string>
<string name="menu_navigation_drawer_recipes_list">Recepten</string>
<string name="fragment_add_recipe_recipe_yield">Recept opbrengst</string>
<string name="fragment_add_recipe_save_button">Recept opslaan</string>
<string name="fragment_add_recipe_new_instruction">Nieuwe stap</string>
<string name="fragment_add_recipe_new_ingredient">Nieuw ingrediënt</string>
<string name="fragment_add_recipe_public_recipe">Publiek recept</string>
<string name="fragment_add_recipe_disable_comments">Opmerkingen uitschakelen</string>
<string name="fragment_add_recipe_ingredient_hint">Ingrediënt</string>
<string name="fragment_add_recipe_instruction_hint">Stapbeschrijving</string>
<string name="fragment_add_recipe_name_error">Receptnaam kan niet leeg zijn</string>
<string name="fragment_add_recipe_save_error">Er ging iets mis</string>
<string name="fragment_add_recipe_save_success">Recept succesvol opgeslagen</string>
<string name="fragment_add_recipe_clear_button">Duidelijk</string>
<string name="fragment_base_url_url_input_helper_text">Voorbeeld: demo.mealie.io</string>
<string name="fragment_authentication_email_input_helper_text">Voorbeeld: changeme@example.com</string>
<string name="fragment_authentication_password_input_helper_text">Voorbeeld: MyPassword</string>
<string name="fragment_recipes_last_page_loaded_toast">Laatste pagina geladen</string>
<string name="fragment_recipes_load_failure_toast" comment="EXAMPLE: Load error: unauthorized.">Fout bij laden: %1$s.</string>
<string name="fragment_recipes_load_failure_toast_no_reason">Laden mislukt.</string>
<string name="fragment_recipes_load_failure_toast_unauthorized">onbevoegd</string>
<string name="fragment_recipes_load_failure_toast_unexpected_response">onverwachte reactie</string>
<string name="fragment_recipes_load_failure_toast_no_connection">geen verbinding</string>
<string name="fragment_recipes_favorite_update_failed">Favoriete statusupdate mislukt</string>
<string name="fragment_recipes_delete_recipe_failed">Verwijderen van recept mislukt</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_title">Recept verwijderen</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_message">Weet je zeker dat je %1$swilt verwijderen? Dit kan niet ongedaan worden gemaakt.</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_positive_btn">Bevestig</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_negative_btn">Annuleren</string>
<string name="menu_navigation_drawer_change_url">URL wijzigen</string>
<string name="search_recipes_hint">Recepten zoeken</string>
<string name="view_toolbar_navigation_icon_content_description">Open de navigatielade</string>
<string name="fragment_recipes_list_no_recipes">Geen recepten</string>
<string name="activity_share_recipe_success_toast">Recept succesvol opgeslagen.</string>
<string name="activity_share_recipe_failure_toast">Er ging iets mis.</string>
<string name="content_description_activity_share_recipe_progress">Voortgangsindicator</string>
<string name="view_holder_recipe_favorite_content_description">Item is favoriet</string>
<string name="view_holder_recipe_non_favorite_content_description">Item is niet favoriet</string>
<string name="view_holder_recipe_delete_content_description">Recept verwijderen</string>
<string name="fragment_recipes_favorite_added">%1$s toegevoegd aan favorieten</string>
<string name="fragment_recipes_favorite_removed">Verwijderde %1$s uit favorieten</string>
<string name="menu_navigation_drawer_shopping_lists">Boodschappenlijstjes</string>
<string name="menu_navigation_drawer_email_logs">Logboeken e-mail</string>
<string name="activity_main_email_logs_subject">Mealient logs</string>
<string name="activity_main_email_logs_confirmation_message">De logs bevatten gevoelige gegevens zoals API-tokens, boodschappenlijsten en recepten. API-tokens kunnen worden ingetrokken met de webclient. Het bestand kan worden bekeken en bewerkt als je het in plaats daarvan naar jezelf stuurt.</string>
<string name="activity_main_email_logs_confirmation_title">Gevoelige gegevens verzenden</string>
<string name="activity_main_email_logs_confirmation_positive">Kies hoe te verzenden</string>
<string name="activity_main_email_logs_confirmation_negative">Annuleren</string>
<string name="activity_main_logout_confirmation_title">Afmelden</string>
<string name="activity_main_logout_confirmation_message">Weet je zeker dat je jezelf wilt afmelden?</string>
<string name="activity_main_logout_confirmation_positive">Afmelden</string>
<string name="activity_main_logout_confirmation_negative">Annuleren</string>
</resources>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="fragment_disclaimer_button_okay_timer">
<item quantity="one">Ok (%d segundo)</item>
<item quantity="other">Ok (%d segundos)</item>
</plurals>
</resources>

View File

@@ -1,83 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fragment_authentication_input_hint_email">E-mail ou nome de utilizador</string>
<string name="fragment_authentication_input_hint_password">Palavra-passe</string>
<string name="fragment_authentication_input_hint_url">URL do servidor</string>
<string name="fragment_authentication_button_login">Iniciar sessão</string>
<string name="content_description_view_holder_recipe_image">Fotografia da refeição cozinhada</string>
<string name="menu_navigation_drawer_logout">Terminar sessão</string>
<string name="view_holder_recipe_text_placeholder">Carregando…</string>
<string name="fragment_recipe_info_ingredients_header">Ingredientes</string>
<string name="fragment_recipe_info_instructions_header">Instruções</string>
<string name="fragment_disclaimer_main_text">Este projeto é desenvolvido independentemente do projeto principal do Mealie. Ele NÃO está associado aos desenvolvedores do Mealie. Quaisquer problemas devem ser reportados ao repositório Mealient, NÃO ao repositório Mealie.</string>
<string name="fragment_baseurl_url_input_empty">O URL não pode estar vazio</string>
<string name="fragment_base_url_no_connection">Não é possível estabelecer ligação, verificar endereço.</string>
<string name="fragment_base_url_unexpected_response">Resposta inesperada. É a Mealie?</string>
<string name="fragment_base_url_malformed_url">Verificar o formato do URL: %s</string>
<string name="fragment_base_url_save">Prosseguir</string>
<string name="fragment_base_url_invalid_certificate_title">A identidade do servidor não pôde ser verificada</string>
<string name="fragment_base_url_invalid_certificate_message">Confia neste certificado?\n\nInformações sobre o certificado:\nEmissor: %1$s\nAssunto: %2$s\nVálido de: %3$s\nVálido até: %4$s</string>
<string name="fragment_base_url_invalid_certificate_accept">Confiança</string>
<string name="fragment_base_url_invalid_certificate_deny">Não</string>
<string name="menu_navigation_drawer_login">Iniciar sessão</string>
<string name="fragment_disclaimer_button_okay">Está bem</string>
<string name="view_holder_recipe_instructions_step">Passo: %d</string>
<string name="fragment_authentication_email_input_empty">O correio eletrónico não pode estar vazio</string>
<string name="fragment_authentication_password_input_empty">A palavra-passe não pode estar vazia</string>
<string name="fragment_authentication_credentials_incorrect">O e-mail ou a palavra-passe estão incorrectos.</string>
<string name="fragment_authentication_unknown_error">Algo correu mal, por favor tente novamente.</string>
<string name="fragment_add_recipe_recipe_name">Nome da receita</string>
<string name="fragment_add_recipe_recipe_description">Descrição</string>
<string name="menu_navigation_drawer_add_recipe">Adicionar receita</string>
<string name="menu_navigation_drawer_recipes_list">Receitas</string>
<string name="fragment_add_recipe_recipe_yield">Rendimento da receita</string>
<string name="fragment_add_recipe_save_button">Guardar receita</string>
<string name="fragment_add_recipe_new_instruction">Nova etapa</string>
<string name="fragment_add_recipe_new_ingredient">Novo ingrediente</string>
<string name="fragment_add_recipe_public_recipe">Receita pública</string>
<string name="fragment_add_recipe_disable_comments">Desativar comentários</string>
<string name="fragment_add_recipe_ingredient_hint">Ingrediente</string>
<string name="fragment_add_recipe_instruction_hint">Descrição das etapas</string>
<string name="fragment_add_recipe_name_error">O nome da receita não pode estar vazio</string>
<string name="fragment_add_recipe_save_error">Algo correu mal</string>
<string name="fragment_add_recipe_save_success">Receita guardada com sucesso</string>
<string name="fragment_add_recipe_clear_button">Limpo</string>
<string name="fragment_base_url_url_input_helper_text">Exemplo: demo.mealie.io</string>
<string name="fragment_authentication_email_input_helper_text">Exemplo: changeme@example.com</string>
<string name="fragment_authentication_password_input_helper_text">Exemplo: MyPassword</string>
<string name="fragment_recipes_last_page_loaded_toast">Última página carregada</string>
<string name="fragment_recipes_load_failure_toast" comment="EXAMPLE: Load error: unauthorized.">Erro de carregamento: %1$s.</string>
<string name="fragment_recipes_load_failure_toast_no_reason">O carregamento falhou.</string>
<string name="fragment_recipes_load_failure_toast_unauthorized">não autorizado</string>
<string name="fragment_recipes_load_failure_toast_unexpected_response">resposta inesperada</string>
<string name="fragment_recipes_load_failure_toast_no_connection">sem ligação</string>
<string name="fragment_recipes_favorite_update_failed">Falha na atualização do estado dos favoritos</string>
<string name="fragment_recipes_delete_recipe_failed">Falha na remoção da receita</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_title">Eliminar receita</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_message">Tem a certeza de que pretende apagar %1$s? Isto não pode ser anulado.</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_positive_btn">Confirmar</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_negative_btn">Cancelar</string>
<string name="menu_navigation_drawer_change_url">Alterar URL</string>
<string name="search_recipes_hint">Pesquisar receitas</string>
<string name="view_toolbar_navigation_icon_content_description">Abrir a gaveta de navegação</string>
<string name="fragment_recipes_list_no_recipes">Sem receitas</string>
<string name="activity_share_recipe_success_toast">Receita guardada com sucesso.</string>
<string name="activity_share_recipe_failure_toast">Alguma coisa correu mal.</string>
<string name="content_description_activity_share_recipe_progress">Indicador de progresso</string>
<string name="view_holder_recipe_favorite_content_description">O item é favorito</string>
<string name="view_holder_recipe_non_favorite_content_description">O item não é favorito</string>
<string name="view_holder_recipe_delete_content_description">Eliminar receita</string>
<string name="fragment_recipes_favorite_added">Adicionado %1$s aos favoritos</string>
<string name="fragment_recipes_favorite_removed">Removido %1$s dos favoritos</string>
<string name="menu_navigation_drawer_shopping_lists">Listas de compras</string>
<string name="menu_navigation_drawer_email_logs">Registos de correio eletrónico</string>
<string name="activity_main_email_logs_subject">Registos de refeições</string>
<string name="activity_main_email_logs_confirmation_message">Os registos contêm dados sensíveis, como o token da API, listas de compras e receitas. Os tokens da API podem ser revogados através do cliente Web. O ficheiro pode ser visualizado e editado se, em vez disso, o enviar para si próprio.</string>
<string name="activity_main_email_logs_confirmation_title">Envio de dados sensíveis</string>
<string name="activity_main_email_logs_confirmation_positive">Escolher como enviar</string>
<string name="activity_main_email_logs_confirmation_negative">Cancelar</string>
<string name="activity_main_logout_confirmation_title">Terminar a sessão</string>
<string name="activity_main_logout_confirmation_message">Tem a certeza de que pretende terminar a sessão?</string>
<string name="activity_main_logout_confirmation_positive">Terminar sessão</string>
<string name="activity_main_logout_confirmation_negative">Cancelar</string>
</resources>

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="fragment_disclaimer_button_okay_timer">
<item quantity="one">Хорошо (%d секунда)</item>
<item quantity="few">Хорошо (%d секунды)</item>
<item quantity="many">Хорошо (%d секунд)</item>
<item quantity="other">Хорошо (%d секунд)</item>
</plurals>
</resources>

View File

@@ -1,83 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fragment_authentication_input_hint_email">Email или username</string>
<string name="fragment_authentication_input_hint_password">Пароль</string>
<string name="fragment_authentication_input_hint_url">URL сервера</string>
<string name="fragment_authentication_button_login">Войти</string>
<string name="content_description_view_holder_recipe_image">Изображение готового блюда</string>
<string name="menu_navigation_drawer_logout">Выйти</string>
<string name="view_holder_recipe_text_placeholder">Загрузка</string>
<string name="fragment_recipe_info_ingredients_header">Ингредиенты</string>
<string name="fragment_recipe_info_instructions_header">Инструкции</string>
<string name="fragment_disclaimer_main_text">Этот проект разрабатывается независимо от основного проекта Meale. Он не связан с разработчиками Mealie. О любых проблемах следует писать в репозиторий Mealient, НЕ в репозиторий Mealie.</string>
<string name="fragment_baseurl_url_input_empty">URL не может быть пустым</string>
<string name="fragment_base_url_no_connection">Ошибка подключения, проверьте адрес.</string>
<string name="fragment_base_url_unexpected_response">Неожиданный ответ. Это Mealie?</string>
<string name="fragment_base_url_malformed_url">Проверьте формат URL: %s</string>
<string name="fragment_base_url_save">Продолжить</string>
<string name="fragment_base_url_invalid_certificate_title">Не удалось проверить подлинность сервера</string>
<string name="fragment_base_url_invalid_certificate_message">Доверяете ли вы этому сертификату?\n\nИнформация о сертификате:\nIssuer: %1$s\nSubject: %2$s\nДействителен с: %3$s\nДействителен до: %4$s</string>
<string name="fragment_base_url_invalid_certificate_accept">Доверять</string>
<string name="fragment_base_url_invalid_certificate_deny">Нет</string>
<string name="menu_navigation_drawer_login">Войти</string>
<string name="fragment_disclaimer_button_okay">Хорошо</string>
<string name="view_holder_recipe_instructions_step">Шаг: %d</string>
<string name="fragment_authentication_email_input_empty">E-mail не может быть пустым</string>
<string name="fragment_authentication_password_input_empty">Пароль не может быть пустым</string>
<string name="fragment_authentication_credentials_incorrect">E-mail или пароль не подходит.</string>
<string name="fragment_authentication_unknown_error">Что-то пошло не так, попробуйте еще раз.</string>
<string name="fragment_add_recipe_recipe_name">Название рецепта</string>
<string name="fragment_add_recipe_recipe_description">Описание</string>
<string name="menu_navigation_drawer_add_recipe">Добавить рецепт</string>
<string name="menu_navigation_drawer_recipes_list">Рецепты</string>
<string name="fragment_add_recipe_recipe_yield">Количество порций</string>
<string name="fragment_add_recipe_save_button">Сохранить рецепт</string>
<string name="fragment_add_recipe_new_instruction">Добавить шаг</string>
<string name="fragment_add_recipe_new_ingredient">Добавить ингредиент</string>
<string name="fragment_add_recipe_public_recipe">Публичный рецепт</string>
<string name="fragment_add_recipe_disable_comments">Отключить комментарии</string>
<string name="fragment_add_recipe_ingredient_hint">Ингредиент</string>
<string name="fragment_add_recipe_instruction_hint">Описание шага</string>
<string name="fragment_add_recipe_name_error">Имя рецепта не может быть пустым</string>
<string name="fragment_add_recipe_save_error">Что-то пошло не так</string>
<string name="fragment_add_recipe_save_success">Рецепт сохранен успешно</string>
<string name="fragment_add_recipe_clear_button">Очистить</string>
<string name="fragment_base_url_url_input_helper_text">Пример: demo.mealie.io</string>
<string name="fragment_authentication_email_input_helper_text">Пример: changeme@example.com</string>
<string name="fragment_authentication_password_input_helper_text">Пример: MyPassword</string>
<string name="fragment_recipes_last_page_loaded_toast">Последняя страница</string>
<string name="fragment_recipes_load_failure_toast" comment="EXAMPLE: Load error: unauthorized.">Ошибка загрузки: %1$s.</string>
<string name="fragment_recipes_load_failure_toast_no_reason">Ошибка загрузки.</string>
<string name="fragment_recipes_load_failure_toast_unauthorized">неавторизован</string>
<string name="fragment_recipes_load_failure_toast_unexpected_response">неожиданный ответ</string>
<string name="fragment_recipes_load_failure_toast_no_connection">нет соединения</string>
<string name="fragment_recipes_favorite_update_failed">Не удалось обновить статус избранного</string>
<string name="fragment_recipes_delete_recipe_failed">Не удалось удалить рецепт</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_title">Удалить рецепт</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_message">Вы уверены, что хотите удалить %1$s? Удаление необратимо.</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_positive_btn">Подтвердить</string>
<string name="fragment_recipes_delete_recipe_confirm_dialog_negative_btn">Отмена</string>
<string name="menu_navigation_drawer_change_url">Сменить URL</string>
<string name="search_recipes_hint">Найти рецепты</string>
<string name="view_toolbar_navigation_icon_content_description">Открыть меню навигации</string>
<string name="fragment_recipes_list_no_recipes">Нет рецептов</string>
<string name="activity_share_recipe_success_toast">Рецепт успешно сохранен.</string>
<string name="activity_share_recipe_failure_toast">Что-то пошло не так.</string>
<string name="content_description_activity_share_recipe_progress">Индикатор прогресса</string>
<string name="view_holder_recipe_favorite_content_description">Добавлен в избранное</string>
<string name="view_holder_recipe_non_favorite_content_description">Не добавлен в избранное</string>
<string name="view_holder_recipe_delete_content_description">Удалить рецепт</string>
<string name="fragment_recipes_favorite_added">%1$s добавлено в избранное</string>
<string name="fragment_recipes_favorite_removed">%1$s удалено из избранного</string>
<string name="menu_navigation_drawer_shopping_lists">Списки покупок</string>
<string name="menu_navigation_drawer_email_logs">Журналы электронной почты</string>
<string name="activity_main_email_logs_subject">Бревна для меалиентов</string>
<string name="activity_main_email_logs_confirmation_message">В журналах содержатся конфиденциальные данные, такие как API-токен, списки покупок и рецепты. API-токены могут быть отозваны с помощью веб-клиента. Файл можно просматривать и редактировать, если отправить его самому себе.</string>
<string name="activity_main_email_logs_confirmation_title">Отправка конфиденциальных данных</string>
<string name="activity_main_email_logs_confirmation_positive">Выберите способ отправки</string>
<string name="activity_main_email_logs_confirmation_negative">Отмена</string>
<string name="activity_main_logout_confirmation_title">Выход из системы</string>
<string name="activity_main_logout_confirmation_message">Вы уверены, что хотите выйти из системы?</string>
<string name="activity_main_logout_confirmation_positive">Выйти из системы</string>
<string name="activity_main_logout_confirmation_negative">Отмена</string>
</resources>

View File

@@ -1,10 +1,10 @@
<resources>
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
<item name="postSplashScreenTheme">@style/AppTheme</item>
<item name="windowSplashScreenBackground">@android:color/white</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_splash_screen</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowSplashScreenBrandingImage">@null</item>
</style>
<style name="AppTheme" parent="Theme.Material3.DynamicColors.DayNight">
@@ -12,5 +12,7 @@
<item name="windowNoTitle">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:enforceStatusBarContrast">false</item>
</style>
</resources>