Add home screen

This commit is contained in:
2026-05-17 20:44:25 +02:00
parent 8700d197f0
commit 8eda4b04ee
9 changed files with 56 additions and 64 deletions

View File

@@ -14,8 +14,8 @@
<string name="auth_error_unknown">Coś poszło nie tak. Spróbuj ponownie.</string> <string name="auth_error_unknown">Coś poszło nie tak. Spróbuj ponownie.</string>
<!-- Phase 2.1 — App shell navigation tab labels (UI-03, CONTEXT D-03) --> <!-- Phase 2.1 — App shell navigation tab labels (UI-03, CONTEXT D-03) -->
<string name="shell_tab_home">Start</string>
<string name="shell_tab_planner">Planer</string> <string name="shell_tab_planner">Planer</string>
<string name="shell_tab_recipes">Przepisy</string>
<string name="shell_tab_pantry">Spiżarnia</string> <string name="shell_tab_pantry">Spiżarnia</string>
<string name="shell_tab_shopping">Zakupy</string> <string name="shell_tab_shopping">Zakupy</string>
@@ -37,10 +37,10 @@
<string name="dock_expand_a11y">Rozwiń pasek nawigacji</string> <string name="dock_expand_a11y">Rozwiń pasek nawigacji</string>
<!-- Phase 2.1 — Empty-state copy (UI-09, CONTEXT D-10/D-11/D-12) --> <!-- Phase 2.1 — Empty-state copy (UI-09, CONTEXT D-10/D-11/D-12) -->
<string name="empty_home_title">Tu pojawi się Twój dzień</string>
<string name="empty_home_subtitle">Wkrótce zobaczysz tu podsumowania i propozycje.</string>
<string name="empty_planner_title">Twój plan tygodnia czeka</string> <string name="empty_planner_title">Twój plan tygodnia czeka</string>
<string name="empty_planner_subtitle">Wkrótce zobaczysz tu zaplanowane posiłki.</string> <string name="empty_planner_subtitle">Wkrótce zobaczysz tu zaplanowane posiłki.</string>
<string name="empty_recipes_title">Tu pojawi się Twoja książka kucharska</string>
<string name="empty_recipes_subtitle">Po dodaniu pierwszych przepisów zobaczysz je w tym miejscu.</string>
<string name="empty_pantry_title">Spiżarnia jest jeszcze pusta</string> <string name="empty_pantry_title">Spiżarnia jest jeszcze pusta</string>
<string name="empty_pantry_subtitle">Wkrótce zobaczysz tu wszystko, co masz pod ręką.</string> <string name="empty_pantry_subtitle">Wkrótce zobaczysz tu wszystko, co masz pod ręką.</string>
<string name="empty_shopping_title">Lista zakupów czeka na Twój plan</string> <string name="empty_shopping_title">Lista zakupów czeka na Twój plan</string>

View File

@@ -1,8 +1,8 @@
package dev.ulfrx.recipe.di package dev.ulfrx.recipe.di
import dev.ulfrx.recipe.ui.screens.home.HomeViewModel
import dev.ulfrx.recipe.ui.screens.pantry.PantryViewModel import dev.ulfrx.recipe.ui.screens.pantry.PantryViewModel
import dev.ulfrx.recipe.ui.screens.planner.PlannerViewModel import dev.ulfrx.recipe.ui.screens.planner.PlannerViewModel
import dev.ulfrx.recipe.ui.screens.recipes.RecipesViewModel
import dev.ulfrx.recipe.ui.screens.search.ShellSearchViewModel import dev.ulfrx.recipe.ui.screens.search.ShellSearchViewModel
import dev.ulfrx.recipe.ui.screens.shopping.ShoppingViewModel import dev.ulfrx.recipe.ui.screens.shopping.ShoppingViewModel
import org.koin.dsl.module import org.koin.dsl.module
@@ -14,8 +14,8 @@ val shellModule =
// Active-tab tracking lives in TabNavigator (a `remember`-scoped state holder // Active-tab tracking lives in TabNavigator (a `remember`-scoped state holder
// owned by AppShell), not in a shell-level VM, so there is no ShellViewModel // owned by AppShell), not in a shell-level VM, so there is no ShellViewModel
// to register. // to register.
viewModel<HomeViewModel>()
viewModel<PlannerViewModel>() viewModel<PlannerViewModel>()
viewModel<RecipesViewModel>()
viewModel<PantryViewModel>() viewModel<PantryViewModel>()
viewModel<ShoppingViewModel>() viewModel<ShoppingViewModel>()

View File

@@ -1,16 +1,16 @@
package dev.ulfrx.recipe.navigation package dev.ulfrx.recipe.navigation
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import com.composables.icons.lucide.BookOpenText
import com.composables.icons.lucide.CalendarDays import com.composables.icons.lucide.CalendarDays
import com.composables.icons.lucide.House
import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Package import com.composables.icons.lucide.Package
import com.composables.icons.lucide.ShoppingCart import com.composables.icons.lucide.ShoppingCart
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.shell_tab_home
import recipe.composeapp.generated.resources.shell_tab_pantry import recipe.composeapp.generated.resources.shell_tab_pantry
import recipe.composeapp.generated.resources.shell_tab_planner import recipe.composeapp.generated.resources.shell_tab_planner
import recipe.composeapp.generated.resources.shell_tab_recipes
import recipe.composeapp.generated.resources.shell_tab_shopping import recipe.composeapp.generated.resources.shell_tab_shopping
enum class DockDestination( enum class DockDestination(
@@ -18,16 +18,16 @@ enum class DockDestination(
val labelRes: StringResource, val labelRes: StringResource,
val icon: ImageVector, val icon: ImageVector,
) { ) {
Home(
startDestination = Screen.Home.Root,
labelRes = Res.string.shell_tab_home,
icon = Lucide.House,
),
Planner( Planner(
startDestination = Screen.Planner.Home, startDestination = Screen.Planner.Home,
labelRes = Res.string.shell_tab_planner, labelRes = Res.string.shell_tab_planner,
icon = Lucide.CalendarDays, icon = Lucide.CalendarDays,
), ),
Recipes(
startDestination = Screen.Recipes.Home,
labelRes = Res.string.shell_tab_recipes,
icon = Lucide.BookOpenText,
),
Pantry( Pantry(
startDestination = Screen.Pantry.Home, startDestination = Screen.Pantry.Home,
labelRes = Res.string.shell_tab_pantry, labelRes = Res.string.shell_tab_pantry,
@@ -41,6 +41,6 @@ enum class DockDestination(
; ;
companion object { companion object {
val Default: DockDestination = Planner val Default: DockDestination = Home
} }
} }

View File

@@ -10,12 +10,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import dev.ulfrx.recipe.ui.screens.home.HomeScreen
import dev.ulfrx.recipe.ui.screens.home.HomeViewModel
import dev.ulfrx.recipe.ui.screens.pantry.PantryScreen import dev.ulfrx.recipe.ui.screens.pantry.PantryScreen
import dev.ulfrx.recipe.ui.screens.pantry.PantryViewModel import dev.ulfrx.recipe.ui.screens.pantry.PantryViewModel
import dev.ulfrx.recipe.ui.screens.planner.PlannerScreen import dev.ulfrx.recipe.ui.screens.planner.PlannerScreen
import dev.ulfrx.recipe.ui.screens.planner.PlannerViewModel import dev.ulfrx.recipe.ui.screens.planner.PlannerViewModel
import dev.ulfrx.recipe.ui.screens.recipes.RecipesScreen
import dev.ulfrx.recipe.ui.screens.recipes.RecipesViewModel
import dev.ulfrx.recipe.ui.screens.shopping.ShoppingScreen import dev.ulfrx.recipe.ui.screens.shopping.ShoppingScreen
import dev.ulfrx.recipe.ui.screens.shopping.ShoppingViewModel import dev.ulfrx.recipe.ui.screens.shopping.ShoppingViewModel
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
@@ -72,14 +72,14 @@ fun RootNavDisplay(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
onBack = { navigator.goBack(tab) }, onBack = { navigator.goBack(tab) },
entryProvider = entryProvider { entryProvider = entryProvider {
entry<Screen.Home.Root> {
val vm: HomeViewModel = koinViewModel()
HomeScreen(viewModel = vm)
}
entry<Screen.Planner.Home> { entry<Screen.Planner.Home> {
val vm: PlannerViewModel = koinViewModel() val vm: PlannerViewModel = koinViewModel()
PlannerScreen(viewModel = vm) PlannerScreen(viewModel = vm)
} }
entry<Screen.Recipes.Home> {
val vm: RecipesViewModel = koinViewModel()
RecipesScreen(viewModel = vm)
}
entry<Screen.Pantry.Home> { entry<Screen.Pantry.Home> {
val vm: PantryViewModel = koinViewModel() val vm: PantryViewModel = koinViewModel()
PantryScreen(viewModel = vm) PantryScreen(viewModel = vm)

View File

@@ -7,22 +7,25 @@ import kotlinx.serialization.Serializable
* Type-safe Nav 3 destinations. Each leaf is a `@Serializable` `NavKey` so the * Type-safe Nav 3 destinations. Each leaf is a `@Serializable` `NavKey` so the
* back stack can be persisted (Nav 3 uses kotlinx-serialization for restoration). * back stack can be persisted (Nav 3 uses kotlinx-serialization for restoration).
* *
* Screens are grouped by tab so future detail destinations (Phase 5+) slot in * Screens are grouped by tab so future detail destinations slot in without
* without polluting the top-level namespace — e.g. `Screen.Recipes.Detail(id)`. * polluting the top-level namespace — e.g. `Screen.Pantry.Detail(id)`. The
* The grouping is purely a code-organisation convenience; Nav 3 treats each * grouping is purely a code-organisation convenience; Nav 3 treats each leaf as
* leaf as an independent NavKey regardless of nesting. * an independent NavKey regardless of nesting.
*
* The Recipes catalog has no own tab — it is reached via the shell-wide search
* destination (see `ShellSearchViewModel`).
*/ */
sealed interface Screen : NavKey { sealed interface Screen : NavKey {
sealed interface Home : Screen {
@Serializable
data object Root : Home
}
sealed interface Planner : Screen { sealed interface Planner : Screen {
@Serializable @Serializable
data object Home : Planner data object Home : Planner
} }
sealed interface Recipes : Screen {
@Serializable
data object Home : Recipes
}
sealed interface Pantry : Screen { sealed interface Pantry : Screen {
@Serializable @Serializable
data object Home : Pantry data object Home : Pantry

View File

@@ -1,4 +1,4 @@
package dev.ulfrx.recipe.ui.screens.recipes package dev.ulfrx.recipe.ui.screens.home
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -19,19 +19,12 @@ import dev.ulfrx.recipe.ui.components.empty.EmptyState
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.empty_recipes_subtitle import recipe.composeapp.generated.resources.empty_home_subtitle
import recipe.composeapp.generated.resources.empty_recipes_title import recipe.composeapp.generated.resources.empty_home_title
import recipe.composeapp.generated.resources.shell_tab_recipes import recipe.composeapp.generated.resources.shell_tab_home
/**
* Phase 2.1 empty-state screen for the Recipes tab. Phase 5 replaces the
* empty body with the recipe catalog grid.
*
* Search is now shell-wide (see `AppShell` + `ShellSearchViewModel`) this
* screen no longer owns any bottom-chrome state.
*/
@Composable @Composable
fun RecipesScreen(viewModel: RecipesViewModel) { fun HomeScreen(viewModel: HomeViewModel) {
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
val state by viewModel.state.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle()
@@ -47,15 +40,15 @@ fun RecipesScreen(viewModel: RecipesViewModel) {
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
) { ) {
BasicText( BasicText(
text = stringResource(Res.string.shell_tab_recipes), text = stringResource(Res.string.shell_tab_home),
style = RecipeTheme.typography.title.copy(color = RecipeTheme.colors.content), style = RecipeTheme.typography.title.copy(color = RecipeTheme.colors.content),
modifier = Modifier.padding(horizontal = RecipeTheme.spacing.lg), modifier = Modifier.padding(horizontal = RecipeTheme.spacing.lg),
) )
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
EmptyState( EmptyState(
icon = DockDestination.Recipes.icon, icon = DockDestination.Home.icon,
title = stringResource(Res.string.empty_recipes_title), title = stringResource(Res.string.empty_home_title),
subtitle = stringResource(Res.string.empty_recipes_subtitle), subtitle = stringResource(Res.string.empty_home_subtitle),
) )
} }
} }

View File

@@ -0,0 +1,15 @@
package dev.ulfrx.recipe.ui.screens.home
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
data class HomeState(
val isEmpty: Boolean = true,
)
class HomeViewModel : ViewModel() {
private val _state = MutableStateFlow(HomeState())
val state: StateFlow<HomeState> = _state.asStateFlow()
}

View File

@@ -1,19 +0,0 @@
package dev.ulfrx.recipe.ui.screens.recipes
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* UI state for [RecipesScreen]. Phase 2.1 ships only the empty state. Phase 5
* (Recipe Catalog Read Path) extends this with `recipes` etc.
*/
data class RecipesState(
val isEmpty: Boolean = true,
)
class RecipesViewModel : ViewModel() {
private val _state = MutableStateFlow(RecipesState())
val state: StateFlow<RecipesState> = _state.asStateFlow()
}

View File

@@ -7,7 +7,7 @@ data object RecipeGlass {
val menu: RecipeGlassStyle = RecipeGlassStyle( val menu: RecipeGlassStyle = RecipeGlassStyle(
refraction = 0.10f, refraction = 0.10f,
curve = 0.5f, curve = 0.5f,
edge = 0.05f, edge = 0.04f,
dispersion = 0.05f, dispersion = 0.05f,
saturation = 0.5f, saturation = 0.5f,
contrast = 1.3f, contrast = 1.3f,