Keep screen on and update docs (#179)

* Extract recipe screen components

* Keep screen on while recipe is shown

* Update screenshots

* Add release note
This commit is contained in:
Kirill Kamakin
2023-11-11 13:35:15 +01:00
committed by GitHub
parent 3941ceb743
commit 7867e2426d
15 changed files with 429 additions and 291 deletions

View File

@@ -30,7 +30,7 @@ information about each of the recipes. Moreover, you can create a recipe from th
## Screenshots
<img src="https://user-images.githubusercontent.com/24299495/203381442-0359cee1-e8a6-4d1f-bdff-eceb1dc31917.png" width="236" height="500" /> <img src="https://user-images.githubusercontent.com/24299495/203381431-51cb57aa-7a2b-4ada-8265-9d382bfae078.png" width="236" height="500" /> <img src="https://user-images.githubusercontent.com/24299495/203381424-358ec3b2-28d9-4237-985d-49be05ef3c7e.png" width="236" height="500" /> <img src="https://user-images.githubusercontent.com/24299495/202909845-d857259f-90f9-4988-beff-038cd784215d.png" width="236" height="500" />
<img src="https://github.com/kirmanak/Mealient/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png?raw=true" width="236" height="500" /> <img src="https://github.com/kirmanak/Mealient/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png?raw=true" width="236" height="500" /> <img src="https://github.com/kirmanak/Mealient/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png?raw=true" width="236" height="500" /> <img src="https://github.com/kirmanak/Mealient/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png?raw=true" width="236" height="500" /> <img src="https://github.com/kirmanak/Mealient/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png?raw=true" width="236" height="500" /> <img src="https://github.com/kirmanak/Mealient/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png?raw=true" width="236" height="500" />
## How to install

View File

@@ -0,0 +1,14 @@
package gq.kirmanak.mealient.extensions
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
fun Context.findActivity(): Activity? {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
return null
}

View File

@@ -0,0 +1,88 @@
package gq.kirmanak.mealient.ui.recipes.info
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.ui.AppTheme
import gq.kirmanak.mealient.ui.Dimens
@Composable
internal fun HeaderSection(
imageUrl: String?,
title: String?,
description: String?,
) {
val imageFallback = painterResource(id = R.drawable.placeholder_recipe)
Column(
verticalArrangement = Arrangement.spacedBy(Dimens.Small, Alignment.Top),
) {
AsyncImage(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(2f) // 2:1
.clip(
RoundedCornerShape(
topEnd = 0.dp,
topStart = 0.dp,
bottomEnd = Dimens.Intermediate,
bottomStart = Dimens.Intermediate,
)
),
model = imageUrl,
contentDescription = stringResource(id = R.string.content_description_fragment_recipe_info_image),
placeholder = imageFallback,
error = imageFallback,
fallback = imageFallback,
contentScale = ContentScale.Crop,
)
if (!title.isNullOrEmpty()) {
Text(
modifier = Modifier
.padding(horizontal = Dimens.Small),
text = title,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
if (!description.isNullOrEmpty()) {
Text(
modifier = Modifier
.padding(horizontal = Dimens.Small),
text = description,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
@Preview
@Composable
private fun HeaderSectionPreview() {
AppTheme {
HeaderSection(
imageUrl = null,
title = "Recipe name",
description = "Recipe description",
)
}
}

View File

@@ -0,0 +1,111 @@
package gq.kirmanak.mealient.ui.recipes.info
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
import gq.kirmanak.mealient.ui.AppTheme
import gq.kirmanak.mealient.ui.Dimens
@Composable
internal fun IngredientsSection(
ingredients: List<RecipeIngredientEntity>,
) {
Column(
verticalArrangement = Arrangement.spacedBy(Dimens.Small, Alignment.Top),
) {
Text(
modifier = Modifier
.padding(horizontal = Dimens.Large),
text = stringResource(id = R.string.fragment_recipe_info_ingredients_header),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = Dimens.Small),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(Dimens.Small),
verticalArrangement = Arrangement.spacedBy(Dimens.Small, Alignment.Top),
) {
ingredients.forEach { item ->
IngredientListItem(
item = item,
)
}
}
}
}
}
@Composable
private fun IngredientListItem(
item: RecipeIngredientEntity,
modifier: Modifier = Modifier,
) {
var isChecked by rememberSaveable { mutableStateOf(false) }
val title = item.title
if (!title.isNullOrBlank()) {
Text(
modifier = modifier
.padding(horizontal = Dimens.Medium),
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
Divider(
color = MaterialTheme.colorScheme.outline,
)
}
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(Dimens.Small, Alignment.Start),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = isChecked,
onCheckedChange = { isChecked = it },
)
Text(
text = item.display,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
@Preview
@Composable
private fun IngredientsSectionPreview() {
AppTheme {
IngredientsSection(
ingredients = INGREDIENTS,
)
}
}

View File

@@ -0,0 +1,118 @@
package gq.kirmanak.mealient.ui.recipes.info
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
import gq.kirmanak.mealient.ui.AppTheme
import gq.kirmanak.mealient.ui.Dimens
@Composable
internal fun InstructionsSection(
instructions: Map<RecipeInstructionEntity, List<RecipeIngredientEntity>>,
) {
Column(
verticalArrangement = Arrangement.spacedBy(Dimens.Small, Alignment.Top),
) {
Text(
modifier = Modifier
.padding(horizontal = Dimens.Large),
text = stringResource(id = R.string.fragment_recipe_info_instructions_header),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
var stepCount = 0
instructions.forEach { (instruction, ingredients) ->
InstructionListItem(
modifier = Modifier
.padding(horizontal = Dimens.Small),
item = instruction,
ingredients = ingredients,
index = stepCount++,
)
}
}
}
@Composable
private fun InstructionListItem(
item: RecipeInstructionEntity,
index: Int,
ingredients: List<RecipeIngredientEntity>,
modifier: Modifier = Modifier,
) {
val title = item.title
if (!title.isNullOrBlank()) {
Text(
modifier = modifier
.padding(horizontal = Dimens.Medium),
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
Card(
modifier = modifier
.fillMaxWidth(),
) {
Column(
modifier = Modifier
.padding(Dimens.Medium),
verticalArrangement = Arrangement.spacedBy(Dimens.Small, Alignment.Top),
) {
Text(
text = stringResource(
R.string.view_holder_recipe_instructions_step,
index + 1
),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = item.text.trim(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
if (ingredients.isNotEmpty()) {
Divider(
color = MaterialTheme.colorScheme.outline,
)
ingredients.forEach { ingredient ->
Text(
text = ingredient.display,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
}
}
@Preview
@Composable
private fun InstructionsSectionPreview() {
AppTheme {
InstructionsSection(
instructions = INSTRUCTIONS,
)
}
}

View File

@@ -0,0 +1,19 @@
package gq.kirmanak.mealient.ui.recipes.info
import android.view.WindowManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalContext
import gq.kirmanak.mealient.extensions.findActivity
@Composable
fun KeepScreenOn() {
val context = LocalContext.current
val window = context.findActivity()?.window ?: return
DisposableEffect(Unit) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
onDispose {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
}

View File

@@ -0,0 +1,64 @@
package gq.kirmanak.mealient.ui.recipes.info
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
internal val INGREDIENT_TWO = RecipeIngredientEntity(
id = "2",
recipeId = "1",
note = "Recipe ingredient note",
food = "Recipe ingredient food",
unit = "Recipe ingredient unit",
display = "Recipe ingredient display that is very long and should be wrapped",
quantity = 1.0,
title = null,
)
internal val SUMMARY_ENTITY = RecipeSummaryEntity(
remoteId = "1",
name = "Recipe name",
slug = "recipe-name",
description = "Recipe description",
dateAdded = LocalDate(2021, 1, 1),
dateUpdated = LocalDateTime(2021, 1, 1, 1, 1, 1),
imageId = null,
isFavorite = false,
)
internal val INGREDIENT_ONE = RecipeIngredientEntity(
id = "1",
recipeId = "1",
note = "Recipe ingredient note",
food = "Recipe ingredient food",
unit = "Recipe ingredient unit",
display = "Recipe ingredient display that is very long and should be wrapped",
quantity = 1.0,
title = "Recipe ingredient section title",
)
internal val INSTRUCTION_ONE = RecipeInstructionEntity(
id = "1",
recipeId = "1",
text = "Recipe instruction",
title = "Section title",
)
internal val INSTRUCTION_TWO = RecipeInstructionEntity(
id = "2",
recipeId = "1",
text = "Recipe instruction",
title = "",
)
internal val INGREDIENTS = listOf(
INGREDIENT_ONE,
INGREDIENT_TWO,
)
internal val INSTRUCTIONS = mapOf(
INSTRUCTION_ONE to emptyList(),
INSTRUCTION_TWO to listOf(INGREDIENT_TWO),
)

View File

@@ -4,45 +4,21 @@ import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
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.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.ui.AppTheme
import gq.kirmanak.mealient.ui.Dimens
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
@Composable
fun RecipeScreen(
uiState: RecipeInfoUiState,
) {
KeepScreenOn()
Column(
modifier = Modifier
.verticalScroll(
@@ -70,217 +46,12 @@ fun RecipeScreen(
}
}
@Composable
private fun HeaderSection(
imageUrl: String?,
title: String?,
description: String?,
) {
val imageFallback = painterResource(id = R.drawable.placeholder_recipe)
AsyncImage(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(2f) // 2:1
.clip(
RoundedCornerShape(
topEnd = 0.dp,
topStart = 0.dp,
bottomEnd = Dimens.Intermediate,
bottomStart = Dimens.Intermediate,
)
),
model = imageUrl,
contentDescription = stringResource(id = R.string.content_description_fragment_recipe_info_image),
placeholder = imageFallback,
error = imageFallback,
fallback = imageFallback,
contentScale = ContentScale.Crop,
)
if (!title.isNullOrEmpty()) {
Text(
modifier = Modifier
.padding(horizontal = Dimens.Small),
text = title,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
if (!description.isNullOrEmpty()) {
Text(
modifier = Modifier
.padding(horizontal = Dimens.Small),
text = description,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
@Composable
private fun InstructionsSection(
instructions: Map<RecipeInstructionEntity, List<RecipeIngredientEntity>>,
) {
Text(
modifier = Modifier
.padding(horizontal = Dimens.Large),
text = stringResource(id = R.string.fragment_recipe_info_instructions_header),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
var stepCount = 0
instructions.forEach { (instruction, ingredients) ->
InstructionListItem(
modifier = Modifier
.padding(horizontal = Dimens.Small),
item = instruction,
ingredients = ingredients,
index = stepCount++,
)
}
}
@Composable
private fun IngredientsSection(
ingredients: List<RecipeIngredientEntity>,
) {
Text(
modifier = Modifier
.padding(horizontal = Dimens.Large),
text = stringResource(id = R.string.fragment_recipe_info_ingredients_header),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = Dimens.Small),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(Dimens.Small),
verticalArrangement = Arrangement.spacedBy(Dimens.Small, Alignment.Top),
) {
ingredients.forEach { item ->
IngredientListItem(
item = item,
)
}
}
}
}
@Composable
private fun InstructionListItem(
item: RecipeInstructionEntity,
index: Int,
ingredients: List<RecipeIngredientEntity>,
modifier: Modifier = Modifier,
) {
val title = item.title
if (!title.isNullOrBlank()) {
Text(
modifier = modifier
.padding(horizontal = Dimens.Medium),
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
Card(
modifier = modifier
.fillMaxWidth(),
) {
Column(
modifier = Modifier
.padding(Dimens.Medium),
verticalArrangement = Arrangement.spacedBy(Dimens.Small, Alignment.Top),
) {
Text(
text = stringResource(
R.string.view_holder_recipe_instructions_step,
index + 1
),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = item.text.trim(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
if (ingredients.isNotEmpty()) {
Divider(
color = MaterialTheme.colorScheme.outline,
)
ingredients.forEach { ingredient ->
Text(
text = ingredient.display,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
}
}
@Composable
private fun IngredientListItem(
item: RecipeIngredientEntity,
modifier: Modifier = Modifier,
) {
var isChecked by rememberSaveable { mutableStateOf(false) }
val title = item.title
if (!title.isNullOrBlank()) {
Text(
modifier = modifier
.padding(horizontal = Dimens.Medium),
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
Divider(
color = MaterialTheme.colorScheme.outline,
)
}
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(Dimens.Small, Alignment.Start),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = isChecked,
onCheckedChange = { isChecked = it },
)
Text(
text = item.display,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
@Preview(showBackground = true)
@Composable
private fun RecipeScreenPreview() {
AppTheme {
RecipeScreen(
uiState = previewUiState()
uiState = RECIPE_INFO_UI_STATE
)
}
}
@@ -290,65 +61,17 @@ private fun RecipeScreenPreview() {
private fun RecipeScreenNightPreview() {
AppTheme {
RecipeScreen(
uiState = previewUiState()
uiState = RECIPE_INFO_UI_STATE
)
}
}
private fun previewUiState(): RecipeInfoUiState {
val ingredient = RecipeIngredientEntity(
id = "2",
recipeId = "1",
note = "Recipe ingredient note",
food = "Recipe ingredient food",
unit = "Recipe ingredient unit",
display = "Recipe ingredient display that is very long and should be wrapped",
quantity = 1.0,
title = null,
)
val uiState = RecipeInfoUiState(
private val RECIPE_INFO_UI_STATE = RecipeInfoUiState(
showIngredients = true,
showInstructions = true,
summaryEntity = RecipeSummaryEntity(
remoteId = "1",
name = "Recipe name",
slug = "recipe-name",
description = "Recipe description",
dateAdded = LocalDate(2021, 1, 1),
dateUpdated = LocalDateTime(2021, 1, 1, 1, 1, 1),
imageId = null,
isFavorite = false,
),
recipeIngredients = listOf(
RecipeIngredientEntity(
id = "1",
recipeId = "1",
note = "Recipe ingredient note",
food = "Recipe ingredient food",
unit = "Recipe ingredient unit",
display = "Recipe ingredient display that is very long and should be wrapped",
quantity = 1.0,
title = "Recipe ingredient section title",
),
ingredient,
),
recipeInstructions = mapOf(
RecipeInstructionEntity(
id = "1",
recipeId = "1",
text = "Recipe instruction",
title = "Section title",
) to emptyList(),
RecipeInstructionEntity(
id = "2",
recipeId = "1",
text = "Recipe instruction",
title = "",
) to listOf(ingredient),
),
summaryEntity = SUMMARY_ENTITY,
recipeIngredients = INGREDIENTS,
recipeInstructions = INSTRUCTIONS,
title = "Recipe title",
description = "Recipe description",
)
return uiState
}

View File

@@ -0,0 +1 @@
Ingredients that are linked to a specific recipe step are shown under that step.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 741 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB