1.0.0 - Material you rework
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
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,
|
||||
dependenciesContainerBuilder = {
|
||||
dependency(baseScreenState)
|
||||
}
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,12 +69,8 @@ internal fun AddRecipeScreen(
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
}
|
||||
|
||||
BaseScreenWithNavigation(
|
||||
baseScreenState = baseScreenState,
|
||||
snackbarHostState = snackbarHostState,
|
||||
) { modifier ->
|
||||
TopProgressIndicator(
|
||||
modifier = modifier,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
isLoading = state.isLoading,
|
||||
) {
|
||||
AddRecipeScreenContent(
|
||||
@@ -88,7 +79,6 @@ internal fun AddRecipeScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddRecipeScreenContent(
|
||||
@@ -304,7 +294,6 @@ private fun AddRecipeInputField(
|
||||
private fun AddRecipeScreenPreview() {
|
||||
AppTheme {
|
||||
AddRecipeScreen(
|
||||
baseScreenState = previewBaseScreenState(),
|
||||
state = AddRecipeScreenState(),
|
||||
onEvent = {},
|
||||
)
|
||||
|
||||
@@ -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,29 +56,15 @@ internal fun BaseURLScreen(
|
||||
@Composable
|
||||
private fun BaseURLScreen(
|
||||
state: BaseURLScreenState,
|
||||
baseScreenState: BaseScreenState,
|
||||
onEvent: (BaseURLScreenEvent) -> Unit,
|
||||
) {
|
||||
val content: @Composable (Modifier) -> Unit = {
|
||||
BaseURLScreen(
|
||||
modifier = it,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = state,
|
||||
onEvent = onEvent,
|
||||
)
|
||||
}
|
||||
|
||||
if (state.isNavigationEnabled) {
|
||||
BaseScreenWithNavigation(
|
||||
baseScreenState = baseScreenState,
|
||||
content = content,
|
||||
)
|
||||
} else {
|
||||
BaseScreen(
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BaseURLScreen(
|
||||
state: BaseURLScreenState,
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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,36 +70,26 @@ 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,
|
||||
)
|
||||
},
|
||||
snackbarHostState = snackbarHostState,
|
||||
) { modifier ->
|
||||
state.snackbarState?.message?.let { message ->
|
||||
|
||||
state.snackbarState?.let { snackbar ->
|
||||
val message = snackbar.message
|
||||
LaunchedEffect(message) {
|
||||
snackbarHostState.showSnackbar(message)
|
||||
onEvent(RecipeListEvent.SnackbarShown)
|
||||
@@ -113,30 +109,104 @@ private fun RecipesList(
|
||||
)
|
||||
}
|
||||
|
||||
BaseScreen(
|
||||
snackbarHostState = snackbarHostState,
|
||||
) { modifier ->
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
// 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,
|
||||
modifier = Modifier.weight(1f),
|
||||
recipes = recipes,
|
||||
onDeleteClick = { itemToDelete = it },
|
||||
onFavoriteClick = { onEvent(RecipeListEvent.FavoriteClick(it)) },
|
||||
onItemClick = { onEvent(RecipeListEvent.RecipeClick(it)) },
|
||||
)
|
||||
}
|
||||
|
||||
isRefreshing -> {
|
||||
CenteredProgressIndicator(
|
||||
modifier = modifier
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
RecipesListError(
|
||||
modifier = modifier,
|
||||
modifier = Modifier.weight(1f),
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
|
||||
import com.atridad.mealient.configureAndroidCompose
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
|
||||
class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
|
||||
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
pluginManager.apply("com.android.application")
|
||||
pluginManager.apply("org.jetbrains.kotlin.plugin.compose")
|
||||
|
||||
extensions.configure<BaseAppModuleExtension> {
|
||||
configureAndroidCompose(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
|
||||
import com.atridad.mealient.Versions
|
||||
import com.atridad.mealient.configureKotlinAndroid
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
|
||||
class AndroidApplicationConventionPlugin : Plugin<Project> {
|
||||
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
with(pluginManager) {
|
||||
apply("com.android.application")
|
||||
apply("org.jetbrains.kotlin.android")
|
||||
apply("org.jetbrains.kotlinx.kover")
|
||||
}
|
||||
|
||||
extensions.configure<BaseAppModuleExtension> {
|
||||
configureKotlinAndroid(this)
|
||||
defaultConfig.targetSdk = Versions.TARGET_SDK_VERSION
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import com.android.build.gradle.LibraryExtension
|
||||
import com.atridad.mealient.configureAndroidCompose
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
|
||||
class AndroidLibraryComposeConventionPlugin : Plugin<Project> {
|
||||
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
pluginManager.apply("org.jetbrains.kotlin.plugin.compose")
|
||||
|
||||
extensions.configure<LibraryExtension> {
|
||||
configureAndroidCompose(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import com.android.build.gradle.LibraryExtension
|
||||
import com.atridad.mealient.Versions
|
||||
import com.atridad.mealient.configureKotlinAndroid
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
|
||||
class AndroidLibraryConventionPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
with(pluginManager) {
|
||||
apply("com.android.library")
|
||||
apply("org.jetbrains.kotlin.android")
|
||||
apply("org.jetbrains.kotlinx.kover")
|
||||
}
|
||||
|
||||
extensions.configure<LibraryExtension> {
|
||||
configureKotlinAndroid(this)
|
||||
defaultConfig.targetSdk = Versions.TARGET_SDK_VERSION
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.atridad.mealient
|
||||
|
||||
import com.android.build.api.dsl.CommonExtension
|
||||
import com.android.build.gradle.LibraryExtension
|
||||
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
internal fun Project.configureAndroidCompose(
|
||||
commonExtension: CommonExtension<*, *, *, *, *, *>,
|
||||
) {
|
||||
val variants = when (commonExtension) {
|
||||
is BaseAppModuleExtension -> commonExtension.applicationVariants
|
||||
is LibraryExtension -> commonExtension.libraryVariants
|
||||
else -> error("Unsupported extension type")
|
||||
}
|
||||
|
||||
commonExtension.apply {
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
// Add compose-destinations generated code to Gradle source sets
|
||||
variants.all {
|
||||
kotlin.sourceSets {
|
||||
getByName(name) {
|
||||
kotlin.srcDir("build/generated/ksp/$name/kotlin")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val bom = library("androidx-compose-bom")
|
||||
add("implementation", platform(bom))
|
||||
add("androidTestImplementation", platform(bom))
|
||||
|
||||
add("implementation", library("androidx-compose-material3"))
|
||||
add("implementation", library("androidx-compose-ui-toolingPreview"))
|
||||
add("implementation", library("androidx-compose-runtime-livedata"))
|
||||
add("implementation", library("androidx-lifecycle-viewmodelCompose"))
|
||||
add("implementation", library("google-accompanist-themeadapter-material3"))
|
||||
add("debugImplementation", library("androidx-compose-ui-tooling"))
|
||||
add("debugImplementation", library("androidx-compose-ui-testManifest"))
|
||||
add("androidTestImplementation", library("androidx-compose-ui-testJunit"))
|
||||
add("implementation", library("composeDestinations-core"))
|
||||
add("ksp", library("composeDestinations-ksp"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.atridad.mealient
|
||||
|
||||
import org.gradle.api.Action
|
||||
import org.gradle.api.NamedDomainObjectContainer
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.MinimalExternalModuleDependency
|
||||
import org.gradle.api.plugins.ExtensionAware
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
|
||||
|
||||
internal val Project.kotlin: KotlinAndroidProjectExtension
|
||||
get() = (this as ExtensionAware).extensions.getByName("kotlin") as KotlinAndroidProjectExtension
|
||||
|
||||
internal fun Project.kotlin(configure: Action<KotlinAndroidProjectExtension>): Unit =
|
||||
(this as ExtensionAware).extensions.configure("kotlin", configure)
|
||||
|
||||
internal fun KotlinAndroidProjectExtension.sourceSets(configure: Action<NamedDomainObjectContainer<KotlinSourceSet>>): Unit =
|
||||
(this as ExtensionAware).extensions.configure("sourceSets", configure)
|
||||
|
||||
internal fun Project.library(name: String): Provider<MinimalExternalModuleDependency> {
|
||||
return libs.findLibrary(name).get()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
package com.atridad.mealient
|
||||
|
||||
import com.android.build.api.dsl.CommonExtension
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
internal fun Project.configureKotlinAndroid(
|
||||
commonExtension: CommonExtension<*, *, *, *, *, *>,
|
||||
) {
|
||||
commonExtension.apply {
|
||||
compileSdk = Versions.COMPILE_SDK_VERSION
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Versions.MIN_SDK_VERSION
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
lint {
|
||||
disable += listOf(
|
||||
"ObsoleteLintCustomCheck",
|
||||
"IconMissingDensityFolder",
|
||||
"MissingTranslation"
|
||||
)
|
||||
enable += listOf(
|
||||
"ConvertToWebp",
|
||||
"DuplicateStrings",
|
||||
"EasterEgg",
|
||||
"ExpensiveAssertion",
|
||||
"IconExpectedSize",
|
||||
"ImplicitSamInstance",
|
||||
"InvalidPackage",
|
||||
"KotlinPropertyAccess",
|
||||
"LambdaLast",
|
||||
"MinSdkTooLow",
|
||||
"NegativeMargin",
|
||||
"NoHardKeywords",
|
||||
"Registered",
|
||||
"RequiredSize",
|
||||
"UnknownNullness",
|
||||
"WrongThreadInterprocedural"
|
||||
)
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
enableUnitTestCoverage = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
add("coreLibraryDesugaring", library("android-tools-desugar").get())
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.atridad.mealient
|
||||
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.VersionCatalog
|
||||
import org.gradle.api.artifacts.VersionCatalogsExtension
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
|
||||
object Versions {
|
||||
const val MIN_SDK_VERSION = 26
|
||||
const val TARGET_SDK_VERSION = 34
|
||||
const val COMPILE_SDK_VERSION = 34
|
||||
}
|
||||
|
||||
val Project.libs: VersionCatalog
|
||||
get() = extensions.getByType<VersionCatalogsExtension>().named("libs")
|
||||
@@ -15,7 +15,7 @@ data class GetRecipeResponse(
|
||||
|
||||
@Serializable
|
||||
data class GetRecipeSettingsResponse(
|
||||
@SerialName("disableAmount") val disableAmount: Boolean,
|
||||
@SerialName("disableAmount") val disableAmount: Boolean = true,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -27,8 +27,8 @@ data class GetRecipeIngredientResponse(
|
||||
@SerialName("display") val display: String,
|
||||
@SerialName("referenceId") val referenceId: String,
|
||||
@SerialName("title") val title: String?,
|
||||
@SerialName("isFood") val isFood: Boolean,
|
||||
@SerialName("disableAmount") val disableAmount: Boolean,
|
||||
@SerialName("isFood") val isFood: Boolean = false,
|
||||
@SerialName("disableAmount") val disableAmount: Boolean = true,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -6,8 +6,10 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
@@ -17,6 +19,8 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.NoMeals
|
||||
import androidx.compose.material.icons.filled.Restaurant
|
||||
import androidx.compose.material3.Checkbox
|
||||
@@ -60,7 +64,7 @@ import com.atridad.mealient.shopping_lists.ui.composables.getErrorMessage
|
||||
import com.atridad.mealient.shopping_lists.util.ItemLabelGroup
|
||||
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.LazyColumnWithLoadingState
|
||||
import com.atridad.mealient.ui.preview.ColorSchemePreview
|
||||
import com.atridad.mealient.ui.util.LoadingState
|
||||
@@ -68,6 +72,7 @@ import com.atridad.mealient.ui.util.data
|
||||
import com.atridad.mealient.ui.util.error
|
||||
import com.atridad.mealient.ui.util.map
|
||||
import java.text.DecimalFormat
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
data class ShoppingListNavArgs(
|
||||
val shoppingListId: String,
|
||||
@@ -82,9 +87,8 @@ internal fun ShoppingListScreen(
|
||||
) {
|
||||
val loadingState by shoppingListViewModel.loadingState.collectAsState()
|
||||
|
||||
BaseScreen { modifier ->
|
||||
ShoppingListScreen(
|
||||
modifier = modifier,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
loadingState = loadingState,
|
||||
errorToShowInSnackbar = shoppingListViewModel.errorToShowInSnackbar,
|
||||
onSnackbarShown = shoppingListViewModel::onSnackbarShown,
|
||||
@@ -99,8 +103,8 @@ internal fun ShoppingListScreen(
|
||||
onAddConfirm = shoppingListViewModel::onAddConfirm,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ShoppingListScreen(
|
||||
loadingState: LoadingState<ShoppingListScreenState>,
|
||||
@@ -117,6 +121,27 @@ private fun ShoppingListScreen(
|
||||
onAddConfirm: (ShoppingListItemState.NewItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val listName = loadingState.data?.name ?: "Shopping List"
|
||||
|
||||
androidx.compose.material3.Scaffold(
|
||||
topBar = {
|
||||
androidx.compose.material3.TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = listName,
|
||||
style = androidx.compose.material3.MaterialTheme.typography.headlineLarge,
|
||||
color = androidx.compose.material3.MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 2,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
colors = androidx.compose.material3.TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = androidx.compose.material3.MaterialTheme.colorScheme.surface,
|
||||
titleContentColor = androidx.compose.material3.MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
val defaultEmptyListError = stringResource(
|
||||
R.string.shopping_list_screen_empty_list,
|
||||
loadingState.data?.name.orEmpty()
|
||||
@@ -134,7 +159,7 @@ private fun ShoppingListScreen(
|
||||
}
|
||||
|
||||
LazyColumnWithLoadingState(
|
||||
modifier = modifier,
|
||||
modifier = modifier.padding(paddingValues),
|
||||
loadingState = loadingState.map { it.items },
|
||||
emptyListError = loadingState.error?.let { getErrorMessage(it) } ?: defaultEmptyListError,
|
||||
retryButtonText = stringResource(id = R.string.shopping_lists_screen_empty_button_refresh),
|
||||
@@ -149,7 +174,6 @@ private fun ShoppingListScreen(
|
||||
onSnackbarShown = onSnackbarShown,
|
||||
onRefresh = onRefreshRequest,
|
||||
floatingActionButton = {
|
||||
// Only show the button if the editor is not active to avoid overlapping
|
||||
if (!itemBeingEdited) {
|
||||
FloatingActionButton(onClick = onAddItemClicked) {
|
||||
Icon(
|
||||
@@ -159,12 +183,25 @@ private fun ShoppingListScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
lazyListState = lazyListState
|
||||
) { sortedItems ->
|
||||
|
||||
lazyListState = lazyListState,
|
||||
lazyColumnContent = { sortedItems ->
|
||||
lastAddedItemIndex = sortedItems.indexOfLast { it is ShoppingListItemState.NewItem }
|
||||
val firstCheckedItemIndex = sortedItems.indexOfFirst { it.checked }
|
||||
|
||||
if (sortedItems.isNotEmpty()) {
|
||||
item(key = "hint") {
|
||||
Text(
|
||||
text = "💡 Swipe left to delete, swipe right to edit",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
modifier = Modifier.padding(
|
||||
horizontal = Dimens.Small,
|
||||
vertical = Dimens.Small
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(sortedItems, { _, item -> item.id}) { index, itemState ->
|
||||
when (itemState) {
|
||||
is ShoppingListItemState.ItemLabel -> {
|
||||
@@ -192,7 +229,6 @@ private fun ShoppingListScreen(
|
||||
onCheckedChange = { onItemCheckedChange(itemState, it) },
|
||||
onDismissed = { onDeleteItem(itemState) },
|
||||
onEditStart = {
|
||||
// Only allow one item to be edited at a time
|
||||
if (!itemBeingEdited) {
|
||||
onEditStart(itemState)
|
||||
}
|
||||
@@ -210,6 +246,8 @@ private fun ShoppingListScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -607,6 +645,7 @@ fun ShoppingListItem(
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Checkbox(
|
||||
checked = itemState.item.checked,
|
||||
@@ -653,7 +692,6 @@ fun ShoppingListItem(
|
||||
if (!isFood) {
|
||||
appendBold(shoppingListItem.note)
|
||||
} else {
|
||||
// Add plural unit and food name if available
|
||||
shoppingListItem.unit?.let { unit ->
|
||||
appendWithPlural(unit.name, unit.pluralName,
|
||||
shoppingListItem.quantity, ::appendWithSpace)
|
||||
@@ -665,22 +703,29 @@ fun ShoppingListItem(
|
||||
}
|
||||
}
|
||||
|
||||
// only show note in secondary text if it's a food item due
|
||||
// to the note already being displayed in the primary text otherwise
|
||||
val secondaryText = shoppingListItem.takeIf { isFood }?.note.orEmpty()
|
||||
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = Dimens.Small)
|
||||
) {
|
||||
Text(
|
||||
text = primaryText,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 2,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||
)
|
||||
if (secondaryText.isNotBlank()) {
|
||||
Text(
|
||||
text = secondaryText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,16 +3,20 @@ package com.atridad.mealient.shopping_lists.ui.list
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ShoppingCart
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -26,21 +30,14 @@ import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.navigation.navigate
|
||||
import com.atridad.mealient.shopping_list.R
|
||||
import com.atridad.mealient.shopping_lists.ui.composables.EditableItemBox
|
||||
import com.atridad.mealient.shopping_lists.ui.composables.getErrorMessage
|
||||
import com.atridad.mealient.shopping_lists.ui.destinations.ShoppingListScreenDestination
|
||||
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.LazyColumnWithLoadingState
|
||||
import com.atridad.mealient.ui.preview.ColorSchemePreview
|
||||
import com.atridad.mealient.ui.util.error
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination
|
||||
@Composable
|
||||
internal fun ShoppingListsScreen(
|
||||
navController: NavController,
|
||||
baseScreenState: BaseScreenState,
|
||||
shoppingListsViewModel: ShoppingListsViewModel = hiltViewModel(),
|
||||
) {
|
||||
val screenState by shoppingListsViewModel.shoppingListsState.collectAsState()
|
||||
@@ -50,18 +47,22 @@ internal fun ShoppingListsScreen(
|
||||
onEvent = shoppingListsViewModel::onEvent
|
||||
)
|
||||
|
||||
BaseScreenWithNavigation(
|
||||
baseScreenState = baseScreenState,
|
||||
) { modifier ->
|
||||
LazyColumnWithLoadingState(
|
||||
modifier = modifier,
|
||||
loadingState = screenState.loadingState,
|
||||
emptyListError = screenState.loadingState.error?.let { getErrorMessage(it) }
|
||||
?: stringResource(R.string.shopping_lists_screen_empty),
|
||||
retryButtonText = stringResource(id = R.string.shopping_lists_screen_empty_button_refresh),
|
||||
snackbarText = screenState.errorToShow?.let { getErrorMessage(error = it) },
|
||||
onSnackbarShown = { shoppingListsViewModel.onEvent(ShoppingListsEvent.SnackbarShown) },
|
||||
onRefresh = { shoppingListsViewModel.onEvent(ShoppingListsEvent.RefreshRequested) },
|
||||
Scaffold(
|
||||
topBar = {
|
||||
androidx.compose.material3.TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Shopping Lists",
|
||||
style = androidx.compose.material3.MaterialTheme.typography.headlineLarge,
|
||||
color = androidx.compose.material3.MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
colors = androidx.compose.material3.TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = androidx.compose.material3.MaterialTheme.colorScheme.surface,
|
||||
titleContentColor = androidx.compose.material3.MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = { shoppingListsViewModel.onEvent(ShoppingListsEvent.AddShoppingList) }
|
||||
@@ -71,13 +72,36 @@ internal fun ShoppingListsScreen(
|
||||
contentDescription = stringResource(id = R.string.shopping_lists_screen_add_icon_content_description),
|
||||
)
|
||||
}
|
||||
},
|
||||
) { items ->
|
||||
items(
|
||||
items = items,
|
||||
key = { it.id },
|
||||
contentType = { "Existing list" }
|
||||
) { displayList ->
|
||||
}
|
||||
) { paddingValues ->
|
||||
// Simple loading state
|
||||
if (screenState.loadingState is com.atridad.mealient.ui.util.LoadingStateNoData.InitialLoad) {
|
||||
Text(
|
||||
text = "Loading...",
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(Dimens.Large)
|
||||
)
|
||||
} else {
|
||||
// Show shopping lists or empty state
|
||||
val shoppingLists = (screenState.loadingState as? com.atridad.mealient.ui.util.LoadingStateWithData.Success)?.data ?: emptyList()
|
||||
|
||||
if (shoppingLists.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.shopping_lists_screen_empty),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(Dimens.Large)
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
items(shoppingLists) { displayList ->
|
||||
ShoppingListCard(
|
||||
listName = displayList.name,
|
||||
onClick = {
|
||||
@@ -95,6 +119,8 @@ internal fun ShoppingListsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShoppingListsScreenDialog(
|
||||
@@ -109,9 +135,7 @@ private fun ShoppingListsScreenDialog(
|
||||
listName = dialog.listName,
|
||||
oldName = dialog.oldListName
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
is ShoppingListsDialog.NewListItem -> {
|
||||
ShoppingListNameDialog(
|
||||
onEvent = onEvent,
|
||||
@@ -119,8 +143,6 @@ private fun ShoppingListsScreenDialog(
|
||||
listName = dialog.listName
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
is ShoppingListsDialog.RemoveListItem -> {
|
||||
DeleteListConfirmDialog(
|
||||
onEvent = onEvent,
|
||||
@@ -128,7 +150,6 @@ private fun ShoppingListsScreenDialog(
|
||||
listName = dialog.listName
|
||||
)
|
||||
}
|
||||
|
||||
is ShoppingListsDialog.None -> {
|
||||
Unit
|
||||
}
|
||||
@@ -179,8 +200,6 @@ private fun ShoppingListCard(
|
||||
imageVector = Icons.Default.ShoppingCart,
|
||||
contentDescription = stringResource(id = R.string.shopping_lists_screen_cart_icon),
|
||||
)
|
||||
|
||||
|
||||
Text(
|
||||
text = listName,
|
||||
)
|
||||
@@ -190,29 +209,3 @@ private fun ShoppingListCard(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ColorSchemePreview
|
||||
private fun PreviewShoppingListCard() {
|
||||
AppTheme {
|
||||
ShoppingListCard(
|
||||
listName = "Weekend shopping",
|
||||
onClick = {},
|
||||
onDelete = {},
|
||||
onEdit = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ColorSchemePreview
|
||||
private fun PreviewEditingShoppingListCard() {
|
||||
AppTheme {
|
||||
ShoppingListCard(
|
||||
listName = "Weekend shopping",
|
||||
onClick = {},
|
||||
onDelete = {},
|
||||
onEdit = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,8 +77,7 @@ fun UserProfileScreen(
|
||||
UserProfileContent(
|
||||
state = state,
|
||||
onEvent = viewModel::onEvent,
|
||||
onSelectImage = { imagePickerLauncher.launch("image/*") },
|
||||
onNavigateBack = { navigator.navigateUp() }
|
||||
onSelectImage = { imagePickerLauncher.launch("image/*") }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -88,53 +87,53 @@ private fun UserProfileContent(
|
||||
state: UserProfileScreenState,
|
||||
onEvent: (ProfileScreenEvent) -> Unit,
|
||||
onSelectImage: () -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
// Top App Bar
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Profile") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
floatingActionButton = {
|
||||
if (!state.isChangingPassword) {
|
||||
if (state.isEditing) {
|
||||
TextButton(onClick = { onEvent(ProfileScreenEvent.CancelEditing) }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
TextButton(
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text("Save") },
|
||||
icon = { Icon(Icons.Default.Check, contentDescription = null) },
|
||||
onClick = { onEvent(ProfileScreenEvent.SaveProfile) },
|
||||
enabled = state.isProfileFormValid && !state.isLoading
|
||||
) {
|
||||
Text("Save")
|
||||
}
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
expanded = true,
|
||||
)
|
||||
} else {
|
||||
IconButton(onClick = { onEvent(ProfileScreenEvent.StartEditing) }) {
|
||||
FloatingActionButton(onClick = { onEvent(ProfileScreenEvent.StartEditing) }) {
|
||||
Icon(Icons.Default.Edit, contentDescription = "Edit")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
) { paddingValues ->
|
||||
// Content
|
||||
if (state.isLoading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[versions]
|
||||
# https://maven.google.com/web/index.html?q=com.android.tools.build#com.android.tools.build:gradle
|
||||
androidGradlePlugin = "8.9.0"
|
||||
androidGradlePlugin = "8.12.2"
|
||||
# https://github.com/JetBrains/kotlin/releases
|
||||
kotlin = "2.0.10"
|
||||
# https://dagger.dev/hilt/gradle-setup
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=f397b287023acdba1e9f6fc5ea72d22dd63669d59ed4a289a29b1a76eee151c6
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
package com.atridad.mealient.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.atridad.mealient.ui.Dimens
|
||||
import com.atridad.mealient.ui.R
|
||||
import com.atridad.mealient.ui.theme.Spacing
|
||||
import com.atridad.mealient.ui.theme.BorderRadius
|
||||
|
||||
interface DrawerItem {
|
||||
|
||||
@@ -32,15 +38,39 @@ internal fun DrawerContent(
|
||||
drawerState: DrawerState,
|
||||
drawerItems: List<DrawerItem>,
|
||||
) {
|
||||
ModalDrawerSheet {
|
||||
Text(
|
||||
ModalDrawerSheet(
|
||||
drawerContainerColor = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.padding(top = Spacing.lg)
|
||||
) {
|
||||
// Header with app branding
|
||||
androidx.compose.foundation.layout.Column(
|
||||
modifier = Modifier
|
||||
.padding(Dimens.Medium),
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = Spacing.lg, vertical = Spacing.xl)
|
||||
) {
|
||||
androidx.compose.material3.Text(
|
||||
text = stringResource(id = R.string.menu_navigation_drawer_header),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
androidx.compose.material3.Text(
|
||||
text = "Your personal recipe collection",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = Spacing.xs)
|
||||
)
|
||||
}
|
||||
|
||||
androidx.compose.material3.HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
thickness = 1.dp,
|
||||
modifier = Modifier.padding(horizontal = Spacing.lg)
|
||||
)
|
||||
|
||||
// Navigation items
|
||||
androidx.compose.foundation.layout.Column(
|
||||
modifier = Modifier.padding(vertical = Spacing.md)
|
||||
) {
|
||||
drawerItems.forEach { item ->
|
||||
NavigationDrawerItem(
|
||||
name = item.getName(),
|
||||
@@ -51,6 +81,7 @@ internal fun DrawerContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NavigationDrawerItem(
|
||||
@@ -62,10 +93,12 @@ private fun NavigationDrawerItem(
|
||||
) {
|
||||
androidx.compose.material3.NavigationDrawerItem(
|
||||
modifier = modifier
|
||||
.padding(horizontal = Dimens.Medium),
|
||||
.padding(horizontal = Spacing.md),
|
||||
label = {
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = if (selected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
selected = selected,
|
||||
@@ -73,8 +106,18 @@ private fun NavigationDrawerItem(
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = if (selected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
onClick = onClick,
|
||||
colors = androidx.compose.material3.NavigationDrawerItemDefaults.colors(
|
||||
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
unselectedContainerColor = androidx.compose.ui.graphics.Color.Transparent,
|
||||
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(BorderRadius.md)
|
||||
)
|
||||
}
|
||||
80
ui/src/main/kotlin/com/atridad/mealient/ui/theme/Color.kt
Normal file
80
ui/src/main/kotlin/com/atridad/mealient/ui/theme/Color.kt
Normal file
@@ -0,0 +1,80 @@
|
||||
package com.atridad.mealient.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
// Light Theme Colors
|
||||
val md_theme_light_primary = Color(0xFF6750A4)
|
||||
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_primaryContainer = Color(0xFFEADDFF)
|
||||
val md_theme_light_onPrimaryContainer = Color(0xFF21005D)
|
||||
val md_theme_light_secondary = Color(0xFF625B71)
|
||||
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_secondaryContainer = Color(0xFFE8DEF8)
|
||||
val md_theme_light_onSecondaryContainer = Color(0xFF1D192B)
|
||||
val md_theme_light_tertiary = Color(0xFF7D5260)
|
||||
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_tertiaryContainer = Color(0xFFFFD8E4)
|
||||
val md_theme_light_onTertiaryContainer = Color(0xFF31111D)
|
||||
val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
val md_theme_light_background = Color(0xFFFFFBFE)
|
||||
val md_theme_light_onBackground = Color(0xFF1C1B1F)
|
||||
val md_theme_light_surface = Color(0xFFFFFBFE)
|
||||
val md_theme_light_onSurface = Color(0xFF1C1B1F)
|
||||
val md_theme_light_surfaceVariant = Color(0xFFE7E0EC)
|
||||
val md_theme_light_onSurfaceVariant = Color(0xFF49454F)
|
||||
val md_theme_light_outline = Color(0xFF79747E)
|
||||
val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4)
|
||||
val md_theme_light_inverseSurface = Color(0xFF313033)
|
||||
val md_theme_light_inversePrimary = Color(0xFFD0BCFF)
|
||||
val md_theme_light_shadow = Color(0xFF000000)
|
||||
val md_theme_light_surfaceTint = Color(0xFF6750A4)
|
||||
val md_theme_light_outlineVariant = Color(0xFFCAC4D0)
|
||||
val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
// Dark Theme Colors
|
||||
val md_theme_dark_primary = Color(0xFFD0BCFF)
|
||||
val md_theme_dark_onPrimary = Color(0xFF381E72)
|
||||
val md_theme_dark_primaryContainer = Color(0xFF4F378B)
|
||||
val md_theme_dark_onPrimaryContainer = Color(0xFFEADDFF)
|
||||
val md_theme_dark_secondary = Color(0xFFCCC2DC)
|
||||
val md_theme_dark_onSecondary = Color(0xFF332D41)
|
||||
val md_theme_dark_secondaryContainer = Color(0xFF4A4458)
|
||||
val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8)
|
||||
val md_theme_dark_tertiary = Color(0xFFEFB8C8)
|
||||
val md_theme_dark_onTertiary = Color(0xFF492532)
|
||||
val md_theme_dark_tertiaryContainer = Color(0xFF633B48)
|
||||
val md_theme_dark_onTertiaryContainer = Color(0xFFFFD8E4)
|
||||
val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
val md_theme_dark_onError = Color(0xFF690005)
|
||||
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
val md_theme_dark_background = Color(0xFF1C1B1F)
|
||||
val md_theme_dark_onBackground = Color(0xFFE6E1E5)
|
||||
val md_theme_dark_surface = Color(0xFF1C1B1F)
|
||||
val md_theme_dark_onSurface = Color(0xFFE6E1E5)
|
||||
val md_theme_dark_surfaceVariant = Color(0xFF49454F)
|
||||
val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4D0)
|
||||
val md_theme_dark_outline = Color(0xFF938F99)
|
||||
val md_theme_dark_inverseOnSurface = Color(0xFF1C1B1F)
|
||||
val md_theme_dark_inverseSurface = Color(0xFFE6E1E5)
|
||||
val md_theme_dark_inversePrimary = Color(0xFF6750A4)
|
||||
val md_theme_dark_shadow = Color(0xFF000000)
|
||||
val md_theme_dark_surfaceTint = Color(0xFFD0BCFF)
|
||||
val md_theme_dark_outlineVariant = Color(0xFF49454F)
|
||||
val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
// Custom App Colors
|
||||
val md_theme_light_food_primary = Color(0xFF2E7D32) // Green for food
|
||||
val md_theme_light_food_secondary = Color(0xFFFF8F00) // Orange for recipes
|
||||
val md_theme_light_food_tertiary = Color(0xFFD32F2F) // Red for cooking
|
||||
val md_theme_light_food_surface = Color(0xFFF8F9FA)
|
||||
val md_theme_light_food_surfaceVariant = Color(0xFFE8F5E8)
|
||||
|
||||
val md_theme_dark_food_primary = Color(0xFF4CAF50) // Green for food
|
||||
val md_theme_dark_food_secondary = Color(0xFFFFB74D) // Orange for recipes
|
||||
val md_theme_dark_food_tertiary = Color(0xFFEF5350) // Red for cooking
|
||||
val md_theme_dark_food_surface = Color(0xFF1A1A1A)
|
||||
val md_theme_dark_food_surfaceVariant = Color(0xFF2E2E2E)
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.atridad.mealient.ui.theme
|
||||
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
// Material 3 Spacing System
|
||||
object Spacing {
|
||||
// Base spacing unit is 4dp
|
||||
val xs = 4.dp
|
||||
val sm = 8.dp
|
||||
val md = 16.dp
|
||||
val lg = 24.dp
|
||||
val xl = 32.dp
|
||||
val xxl = 48.dp
|
||||
val xxxl = 64.dp
|
||||
}
|
||||
|
||||
// Material 3 Component Sizing
|
||||
object ComponentSizing {
|
||||
// Button heights
|
||||
val buttonHeight = 40.dp
|
||||
val buttonHeightLarge = 48.dp
|
||||
val buttonHeightSmall = 32.dp
|
||||
|
||||
// Input field heights
|
||||
val inputFieldHeight = 56.dp
|
||||
val inputFieldHeightSmall = 48.dp
|
||||
|
||||
// Card dimensions
|
||||
val cardElevation = 1.dp
|
||||
val cardElevationHovered = 8.dp
|
||||
val cardElevationPressed = 12.dp
|
||||
|
||||
// Icon sizes
|
||||
val iconSize = 24.dp
|
||||
val iconSizeSmall = 20.dp
|
||||
val iconSizeLarge = 32.dp
|
||||
|
||||
// Avatar sizes
|
||||
val avatarSize = 40.dp
|
||||
val avatarSizeSmall = 32.dp
|
||||
val avatarSizeLarge = 56.dp
|
||||
|
||||
// FAB sizes
|
||||
val fabSize = 56.dp
|
||||
val fabSizeSmall = 40.dp
|
||||
val fabSizeLarge = 96.dp
|
||||
}
|
||||
|
||||
// Material 3 Border Radius
|
||||
object BorderRadius {
|
||||
val xs = 4.dp
|
||||
val sm = 8.dp
|
||||
val md = 12.dp
|
||||
val lg = 16.dp
|
||||
val xl = 20.dp
|
||||
val xxl = 28.dp
|
||||
val full = 50.dp
|
||||
}
|
||||
|
||||
// Material 3 Content Padding
|
||||
object ContentPadding {
|
||||
val xs = 4.dp
|
||||
val sm = 8.dp
|
||||
val md = 16.dp
|
||||
val lg = 24.dp
|
||||
val xl = 32.dp
|
||||
val xxl = 48.dp
|
||||
}
|
||||
|
||||
// Material 3 List Item Spacing
|
||||
object ListSpacing {
|
||||
val itemSpacing = 8.dp
|
||||
val sectionSpacing = 16.dp
|
||||
val listPadding = 16.dp
|
||||
}
|
||||
|
||||
// Material 3 Screen Margins
|
||||
object ScreenMargins {
|
||||
val horizontal = 16.dp
|
||||
val horizontalLarge = 24.dp
|
||||
val vertical = 16.dp
|
||||
val verticalLarge = 24.dp
|
||||
}
|
||||
144
ui/src/main/kotlin/com/atridad/mealient/ui/theme/Theme.kt
Normal file
144
ui/src/main/kotlin/com/atridad/mealient/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,144 @@
|
||||
package com.atridad.mealient.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
// Primary colors
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
|
||||
// Secondary colors
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
|
||||
// Tertiary colors
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
|
||||
// Error colors
|
||||
error = md_theme_light_error,
|
||||
onError = md_theme_light_onError,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
|
||||
// Neutral colors
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
|
||||
// Outline colors
|
||||
outline = md_theme_light_outline,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
|
||||
// Inverse colors
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
|
||||
// Surface colors
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
// Primary colors
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
|
||||
// Secondary colors
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
|
||||
// Tertiary colors
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
|
||||
// Error colors
|
||||
error = md_theme_dark_error,
|
||||
onError = md_theme_dark_onError,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
|
||||
// Neutral colors
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
|
||||
// Outline colors
|
||||
outline = md_theme_dark_outline,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
|
||||
// Inverse colors
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
|
||||
// Surface colors
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun MealientTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
// Make status bar transparent so it blends with the background
|
||||
window.statusBarColor = android.graphics.Color.TRANSPARENT
|
||||
// Set light status bar icons for dark theme, dark icons for light theme
|
||||
// This ensures proper contrast for the status bar icons
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
125
ui/src/main/kotlin/com/atridad/mealient/ui/theme/Type.kt
Normal file
125
ui/src/main/kotlin/com/atridad/mealient/ui/theme/Type.kt
Normal file
@@ -0,0 +1,125 @@
|
||||
package com.atridad.mealient.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Material 3 Typography Scale
|
||||
val Typography = Typography(
|
||||
// Display styles - Large text for hero sections
|
||||
displayLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 64.sp,
|
||||
letterSpacing = (-0.25).sp,
|
||||
),
|
||||
displayMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 45.sp,
|
||||
lineHeight = 52.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
displaySmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 36.sp,
|
||||
lineHeight = 44.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
|
||||
// Headline styles - Section titles
|
||||
headlineLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 40.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
headlineMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 36.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
headlineSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 32.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
|
||||
// Title styles - Card titles, list headers
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.15.sp,
|
||||
),
|
||||
titleSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp,
|
||||
),
|
||||
|
||||
// Body styles - Main content text
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.25.sp,
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.4.sp,
|
||||
),
|
||||
|
||||
// Label styles - Buttons, form fields, captions
|
||||
labelLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp,
|
||||
),
|
||||
labelMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
)
|
||||
Reference in New Issue
Block a user