Restyle recipe details top bar
This commit is contained in:
@@ -50,6 +50,7 @@
|
|||||||
<string name="recipe_detail_servings_increment_a11y">Zwiększ liczbę porcji</string>
|
<string name="recipe_detail_servings_increment_a11y">Zwiększ liczbę porcji</string>
|
||||||
<string name="sheet_drag_handle_a11y">Przeciągnij w dół, aby zamknąć</string>
|
<string name="sheet_drag_handle_a11y">Przeciągnij w dół, aby zamknąć</string>
|
||||||
<string name="recipe_detail_not_found">Nie znaleziono przepisu</string>
|
<string name="recipe_detail_not_found">Nie znaleziono przepisu</string>
|
||||||
|
<string name="recipe_detail_close_a11y">Zamknij szczegóły przepisu</string>
|
||||||
<string name="meal_plan_editor_not_found">Nie udało się otworzyć edytora</string>
|
<string name="meal_plan_editor_not_found">Nie udało się otworzyć edytora</string>
|
||||||
|
|
||||||
<!-- Phase 2.1 — Search affordance a11y (UI-10, CONTEXT D-06/D-08) -->
|
<!-- Phase 2.1 — Search affordance a11y (UI-10, CONTEXT D-06/D-08) -->
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -47,7 +49,14 @@ fun <T : NavKey> RecipeBottomSheet(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
entries: EntryProviderScope<T>.() -> Unit,
|
entries: EntryProviderScope<T>.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val modalSheetState = rememberModalBottomSheetState(initialDetent = SheetDetent.Hidden, detents = listOf(SheetDetent.Hidden, SheetDetent.FullyExpanded))
|
val dismissalGate = remember { SheetDismissalGate() }
|
||||||
|
val modalSheetState = rememberModalBottomSheetState(
|
||||||
|
initialDetent = SheetDetent.Hidden,
|
||||||
|
detents = listOf(SheetDetent.Hidden, SheetDetent.FullyExpanded),
|
||||||
|
confirmDetentChange = { detent ->
|
||||||
|
detent != SheetDetent.Hidden || !dismissalGate.isBlocked
|
||||||
|
},
|
||||||
|
)
|
||||||
val saveableDecorator = rememberSaveableStateHolderNavEntryDecorator<T>()
|
val saveableDecorator = rememberSaveableStateHolderNavEntryDecorator<T>()
|
||||||
val viewModelDecorator = rememberViewModelStoreNavEntryDecorator<T>()
|
val viewModelDecorator = rememberViewModelStoreNavEntryDecorator<T>()
|
||||||
|
|
||||||
@@ -62,9 +71,11 @@ fun <T : NavKey> RecipeBottomSheet(
|
|||||||
)
|
)
|
||||||
Sheet(
|
Sheet(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
enabled = !dismissalGate.isBlocked,
|
||||||
backgroundColor = RecipeTheme.colors.background,
|
backgroundColor = RecipeTheme.colors.background,
|
||||||
shape = RoundedCornerShape(topStart = SHEET_CORNER_RADIUS, topEnd = SHEET_CORNER_RADIUS),
|
shape = RoundedCornerShape(topStart = SHEET_CORNER_RADIUS, topEnd = SHEET_CORNER_RADIUS),
|
||||||
) {
|
) {
|
||||||
|
CompositionLocalProvider(LocalSheetDismissalGate provides dismissalGate) {
|
||||||
SheetBody {
|
SheetBody {
|
||||||
if (state.backStack.isNotEmpty()) {
|
if (state.backStack.isNotEmpty()) {
|
||||||
NavDisplay(
|
NavDisplay(
|
||||||
@@ -81,6 +92,7 @@ fun <T : NavKey> RecipeBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BottomSheetScope.SheetBody(content: @Composable BoxScope.() -> Unit) {
|
private fun BottomSheetScope.SheetBody(content: @Composable BoxScope.() -> Unit) {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package dev.ulfrx.recipe.ui.components.sheet
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counter-based gate that disables user-initiated sheet dismissal (drag,
|
||||||
|
* scrim tap) while at least one consumer holds a lock. Programmatic close via
|
||||||
|
* [RecipeBottomSheetState.dismiss] always succeeds.
|
||||||
|
*/
|
||||||
|
@Stable
|
||||||
|
class SheetDismissalGate {
|
||||||
|
private var lockCount by mutableStateOf(0)
|
||||||
|
|
||||||
|
val isBlocked: Boolean by derivedStateOf { lockCount > 0 }
|
||||||
|
|
||||||
|
internal fun acquire() {
|
||||||
|
lockCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun release() {
|
||||||
|
if (lockCount > 0) lockCount--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val LocalSheetDismissalGate = compositionLocalOf<SheetDismissalGate?> { null }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks gesture-initiated dismissal of the enclosing [RecipeBottomSheet]
|
||||||
|
* while this composable is in composition. Programmatic close via
|
||||||
|
* [RecipeBottomSheetState.dismiss] still works.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun BlockSheetDismiss() {
|
||||||
|
val gate = LocalSheetDismissalGate.current ?: return
|
||||||
|
DisposableEffect(gate) {
|
||||||
|
gate.acquire()
|
||||||
|
onDispose { gate.release() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import dev.ulfrx.recipe.ui.components.glass.CircleGlassButton
|
|||||||
import dev.ulfrx.recipe.ui.components.glass.GlassBackdropSource
|
import dev.ulfrx.recipe.ui.components.glass.GlassBackdropSource
|
||||||
import dev.ulfrx.recipe.ui.components.glass.LocalGlassBackdropState
|
import dev.ulfrx.recipe.ui.components.glass.LocalGlassBackdropState
|
||||||
import dev.ulfrx.recipe.ui.components.glass.rememberGlassBackdropState
|
import dev.ulfrx.recipe.ui.components.glass.rememberGlassBackdropState
|
||||||
|
import dev.ulfrx.recipe.ui.components.sheet.BlockSheetDismiss
|
||||||
import dev.ulfrx.recipe.ui.theme.RecipeTheme
|
import dev.ulfrx.recipe.ui.theme.RecipeTheme
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import recipe.composeapp.generated.resources.Res
|
import recipe.composeapp.generated.resources.Res
|
||||||
@@ -48,6 +49,7 @@ internal fun MealPlanEditorScreen(
|
|||||||
) {
|
) {
|
||||||
when (val s = state) {
|
when (val s = state) {
|
||||||
is MealPlanEditorState.Editing -> {
|
is MealPlanEditorState.Editing -> {
|
||||||
|
BlockSheetDismiss()
|
||||||
GlassBackdropSource(state = backdrop, modifier = Modifier.fillMaxSize()) {
|
GlassBackdropSource(state = backdrop, modifier = Modifier.fillMaxSize()) {
|
||||||
MealPlanEditorContent(
|
MealPlanEditorContent(
|
||||||
editing = s,
|
editing = s,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package dev.ulfrx.recipe.ui.screens.recipedetail
|
package dev.ulfrx.recipe.ui.screens.recipedetail
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -19,6 +20,9 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import dev.ulfrx.recipe.ui.components.recipe.IngredientCard
|
import dev.ulfrx.recipe.ui.components.recipe.IngredientCard
|
||||||
@@ -45,9 +49,10 @@ import recipe.composeapp.generated.resources.recipe_detail_step_number_format
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun RecipeDetailContent(
|
internal fun RecipeDetailContent(
|
||||||
ready: RecipeDetailState.Ready,
|
ready: RecipeDetailState.Ready,
|
||||||
onPlanClick: () -> Unit,
|
|
||||||
onServingsChange: (Int) -> Unit,
|
onServingsChange: (Int) -> Unit,
|
||||||
onSelectSubstitution: (slotId: String, optionId: String) -> Unit,
|
onSelectSubstitution: (slotId: String, optionId: String) -> Unit,
|
||||||
|
topChromeInset: Dp,
|
||||||
|
topChromeHeight: Dp,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val spacing = RecipeTheme.spacing
|
val spacing = RecipeTheme.spacing
|
||||||
@@ -58,11 +63,35 @@ internal fun RecipeDetailContent(
|
|||||||
val servings = ready.servings
|
val servings = ready.servings
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxSize().verticalScroll(scrollState)) {
|
Column(modifier = modifier.fillMaxSize().verticalScroll(scrollState)) {
|
||||||
RecipeDetailHero(
|
Spacer(Modifier.height(topChromeInset))
|
||||||
title = detail.title,
|
// Aligns the title row with the floating close chrome at scroll=0:
|
||||||
cookingMinutes = detail.cookingMinutes,
|
// same height, padded inside the chrome's circle pill.
|
||||||
onPlanClick = onPlanClick,
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(topChromeHeight)
|
||||||
|
.padding(horizontal = spacing.lg + topChromeHeight + spacing.sm),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = detail.title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style =
|
||||||
|
RecipeTheme.typography.body.copy(
|
||||||
|
color = RecipeTheme.colors.content,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = TitleSize,
|
||||||
|
lineHeight = TitleLineHeight,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(spacing.xl))
|
||||||
|
|
||||||
|
RecipeDetailHero(cookingMinutes = detail.cookingMinutes)
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = spacing.lg)) {
|
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = spacing.lg)) {
|
||||||
Spacer(Modifier.height(spacing.xl))
|
Spacer(Modifier.height(spacing.xl))
|
||||||
@@ -183,6 +212,9 @@ private fun StepRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val TitleSize = 16.sp
|
||||||
|
private val TitleLineHeight = 17.sp
|
||||||
|
|
||||||
private val StepNumberWidth = 20.dp
|
private val StepNumberWidth = 20.dp
|
||||||
private val StepNumberTextSize = 11.sp
|
private val StepNumberTextSize = 11.sp
|
||||||
private val StepTextSize = 13.sp
|
private val StepTextSize = 13.sp
|
||||||
|
|||||||
@@ -22,32 +22,22 @@ import androidx.compose.ui.draw.shadow
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import com.composables.icons.lucide.Calendar
|
|
||||||
import com.composables.icons.lucide.Clock
|
import com.composables.icons.lucide.Clock
|
||||||
import com.composables.icons.lucide.Lucide
|
import com.composables.icons.lucide.Lucide
|
||||||
import com.composeunstyled.UnstyledIcon
|
import com.composeunstyled.UnstyledIcon
|
||||||
import dev.ulfrx.recipe.ui.components.button.CircleButton
|
|
||||||
import dev.ulfrx.recipe.ui.theme.RecipeTheme
|
import dev.ulfrx.recipe.ui.theme.RecipeTheme
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import recipe.composeapp.generated.resources.Res
|
import recipe.composeapp.generated.resources.Res
|
||||||
import recipe.composeapp.generated.resources.meal_plan_editor_title_a11y
|
|
||||||
import recipe.composeapp.generated.resources.recipe_card_minutes_format
|
import recipe.composeapp.generated.resources.recipe_card_minutes_format
|
||||||
import recipe.composeapp.generated.resources.sample_recipe
|
import recipe.composeapp.generated.resources.sample_recipe
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RecipeDetailHero(
|
internal fun RecipeDetailHero(
|
||||||
title: String,
|
|
||||||
cookingMinutes: Int,
|
cookingMinutes: Int,
|
||||||
onPlanClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val colors = RecipeTheme.colors
|
|
||||||
val typography = RecipeTheme.typography
|
|
||||||
val spacing = RecipeTheme.spacing
|
val spacing = RecipeTheme.spacing
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -55,7 +45,6 @@ internal fun RecipeDetailHero(
|
|||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(
|
.padding(
|
||||||
top = HERO_TOP_PADDING,
|
|
||||||
bottom = spacing.lg,
|
bottom = spacing.lg,
|
||||||
start = spacing.lg,
|
start = spacing.lg,
|
||||||
end = spacing.lg,
|
end = spacing.lg,
|
||||||
@@ -81,33 +70,13 @@ internal fun RecipeDetailHero(
|
|||||||
|
|
||||||
Spacer(Modifier.height(spacing.lg))
|
Spacer(Modifier.height(spacing.lg))
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement =Arrangement.spacedBy(spacing.lg),
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = title,
|
|
||||||
style =
|
|
||||||
typography.display.copy(
|
|
||||||
color = colors.content,
|
|
||||||
fontSize = TITLE_FONT_SIZE,
|
|
||||||
lineHeight = TITLE_LINE_HEIGHT,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
textAlign = TextAlign.Left,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(spacing.sm)) {
|
|
||||||
MetaChip(
|
MetaChip(
|
||||||
icon = Lucide.Clock,
|
icon = Lucide.Clock,
|
||||||
text = stringResource(Res.string.recipe_card_minutes_format, cookingMinutes),
|
text = stringResource(Res.string.recipe_card_minutes_format, cookingMinutes),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PlanButton(onClick = onPlanClick)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -139,38 +108,13 @@ private fun MetaChip(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PlanButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
CircleButton(
|
|
||||||
onClick = onClick,
|
|
||||||
icon = Lucide.Calendar,
|
|
||||||
contentDescription = stringResource(Res.string.meal_plan_editor_title_a11y),
|
|
||||||
size = PLAN_BUTTON_SIZE,
|
|
||||||
iconSize = PLAN_BUTTON_ICON_SIZE,
|
|
||||||
tint = RecipeTheme.colors.surface,
|
|
||||||
iconTint = RecipeTheme.colors.accent,
|
|
||||||
borderTint = RecipeTheme.colors.borderCard,
|
|
||||||
borderWidth = 1.dp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val BANNER_ASPECT_RATIO = 16f / 9f
|
private const val BANNER_ASPECT_RATIO = 16f / 9f
|
||||||
private val BANNER_CORNER = 20.dp
|
private val BANNER_CORNER = 20.dp
|
||||||
private val BANNER_SHADOW_ELEVATION = 14.dp
|
private val BANNER_SHADOW_ELEVATION = 14.dp
|
||||||
private val BANNER_SHADOW_COLOR = Color.Black.copy(alpha = 0.45f)
|
private val BANNER_SHADOW_COLOR = Color.Black.copy(alpha = 0.45f)
|
||||||
|
|
||||||
// Leave room for the sheet handle (8dp top padding + 5dp handle) plus breathing room.
|
|
||||||
private val HERO_TOP_PADDING = 32.dp
|
|
||||||
|
|
||||||
private val TITLE_FONT_SIZE = 19.sp
|
|
||||||
private val TITLE_LINE_HEIGHT = 20.sp
|
|
||||||
|
|
||||||
private val CHIP_SHAPE = RoundedCornerShape(percent = 50)
|
private val CHIP_SHAPE = RoundedCornerShape(percent = 50)
|
||||||
private val CHIP_PADDING_H = 12.dp
|
private val CHIP_PADDING_H = 12.dp
|
||||||
private val CHIP_PADDING_V = 7.dp
|
private val CHIP_PADDING_V = 7.dp
|
||||||
private val CHIP_ICON_SIZE = 14.dp
|
private val CHIP_ICON_SIZE = 14.dp
|
||||||
private val CHIP_ICON_GAP = 5.dp
|
private val CHIP_ICON_GAP = 5.dp
|
||||||
private val PLAN_BUTTON_SIZE = 50.dp
|
|
||||||
private val PLAN_BUTTON_ICON_SIZE = 25.dp
|
|
||||||
|
|||||||
@@ -1,39 +1,99 @@
|
|||||||
package dev.ulfrx.recipe.ui.screens.recipedetail
|
package dev.ulfrx.recipe.ui.screens.recipedetail
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.text.BasicText
|
import androidx.compose.foundation.text.BasicText
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.composables.icons.lucide.Calendar
|
||||||
|
import com.composables.icons.lucide.Lucide
|
||||||
|
import com.composables.icons.lucide.X
|
||||||
|
import dev.ulfrx.recipe.ui.components.glass.CircleGlassButton
|
||||||
|
import dev.ulfrx.recipe.ui.components.glass.GlassBackdropSource
|
||||||
|
import dev.ulfrx.recipe.ui.components.glass.LocalGlassBackdropState
|
||||||
|
import dev.ulfrx.recipe.ui.components.glass.rememberGlassBackdropState
|
||||||
import dev.ulfrx.recipe.ui.theme.RecipeTheme
|
import dev.ulfrx.recipe.ui.theme.RecipeTheme
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import recipe.composeapp.generated.resources.Res
|
import recipe.composeapp.generated.resources.Res
|
||||||
|
import recipe.composeapp.generated.resources.meal_plan_editor_title_a11y
|
||||||
|
import recipe.composeapp.generated.resources.recipe_detail_close_a11y
|
||||||
import recipe.composeapp.generated.resources.recipe_detail_not_found
|
import recipe.composeapp.generated.resources.recipe_detail_not_found
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RecipeDetailScreen(
|
internal fun RecipeDetailScreen(
|
||||||
viewModel: RecipeDetailViewModel,
|
viewModel: RecipeDetailViewModel,
|
||||||
onPlan: (RecipeDetailState.Ready) -> Unit,
|
onPlan: (RecipeDetailState.Ready) -> Unit,
|
||||||
|
onClose: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val backdrop = rememberGlassBackdropState()
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val spacing = RecipeTheme.spacing
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalGlassBackdropState provides backdrop) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(RecipeTheme.colors.background),
|
||||||
|
) {
|
||||||
when (val s = state) {
|
when (val s = state) {
|
||||||
is RecipeDetailState.Ready ->
|
is RecipeDetailState.Ready -> {
|
||||||
|
GlassBackdropSource(state = backdrop, modifier = Modifier.fillMaxSize()) {
|
||||||
RecipeDetailContent(
|
RecipeDetailContent(
|
||||||
ready = s,
|
ready = s,
|
||||||
onPlanClick = { onPlan(s) },
|
|
||||||
onServingsChange = viewModel::setServings,
|
onServingsChange = viewModel::setServings,
|
||||||
onSelectSubstitution = viewModel::selectSubstitution,
|
onSelectSubstitution = viewModel::selectSubstitution,
|
||||||
|
topChromeInset = TopActionsTopInset,
|
||||||
|
topChromeHeight = TopPillHeight,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
RecipeDetailState.NotFound ->
|
CircleGlassButton(
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
onClick = { onPlan(s) },
|
||||||
|
icon = Lucide.Calendar,
|
||||||
|
contentDescription = stringResource(Res.string.meal_plan_editor_title_a11y),
|
||||||
|
size = TopPillHeight,
|
||||||
|
iconSize = TopActionIconSize,
|
||||||
|
glassStyle = RecipeTheme.glass.button,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(top = TopActionsTopInset, end = spacing.lg),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
RecipeDetailState.NotFound -> {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = stringResource(Res.string.recipe_detail_not_found),
|
text = stringResource(Res.string.recipe_detail_not_found),
|
||||||
style = RecipeTheme.typography.body,
|
style = RecipeTheme.typography.body,
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CircleGlassButton(
|
||||||
|
onClick = onClose,
|
||||||
|
icon = Lucide.X,
|
||||||
|
contentDescription = stringResource(Res.string.recipe_detail_close_a11y),
|
||||||
|
size = TopPillHeight,
|
||||||
|
iconSize = TopActionIconSize,
|
||||||
|
glassStyle = RecipeTheme.glass.button,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.padding(top = TopActionsTopInset, start = spacing.lg),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val TopPillHeight = 44.dp
|
||||||
|
private val TopActionIconSize = 18.dp
|
||||||
|
private val TopActionsTopInset = 28.dp
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ fun RecipeSheet(state: RecipeBottomSheetState<Screen>) {
|
|||||||
|
|
||||||
RecipeDetailScreen(
|
RecipeDetailScreen(
|
||||||
viewModel = vm,
|
viewModel = vm,
|
||||||
|
onClose = state::dismiss,
|
||||||
onPlan = { ready ->
|
onPlan = { ready ->
|
||||||
state.push(
|
state.push(
|
||||||
Screen.MealPlanEditor.Open(
|
Screen.MealPlanEditor.Open(
|
||||||
|
|||||||
Reference in New Issue
Block a user