Add search for every screen

This commit is contained in:
2026-05-12 23:09:39 +02:00
parent 8f4903a055
commit 4a9cba02d6
7 changed files with 126 additions and 6 deletions

View File

@@ -22,6 +22,8 @@
<!-- Phase 2.1 — Search affordance placeholders (UI-10, CONTEXT D-06) --> <!-- Phase 2.1 — Search affordance placeholders (UI-10, CONTEXT D-06) -->
<string name="search_placeholder_recipes">Szukaj przepisów…</string> <string name="search_placeholder_recipes">Szukaj przepisów…</string>
<string name="search_placeholder_pantry">Szukaj w spiżarni…</string> <string name="search_placeholder_pantry">Szukaj w spiżarni…</string>
<string name="search_placeholder_planner">Szukaj w planie…</string>
<string name="search_placeholder_shopping">Szukaj na liście…</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) -->
<string name="search_open_a11y">Otwórz wyszukiwanie</string> <string name="search_open_a11y">Otwórz wyszukiwanie</string>

View File

@@ -2,9 +2,11 @@ package dev.ulfrx.recipe.di
import dev.ulfrx.recipe.ui.screens.pantry.PantrySearchViewModel import dev.ulfrx.recipe.ui.screens.pantry.PantrySearchViewModel
import dev.ulfrx.recipe.ui.screens.pantry.PantryViewModel import dev.ulfrx.recipe.ui.screens.pantry.PantryViewModel
import dev.ulfrx.recipe.ui.screens.planner.PlannerSearchViewModel
import dev.ulfrx.recipe.ui.screens.planner.PlannerViewModel import dev.ulfrx.recipe.ui.screens.planner.PlannerViewModel
import dev.ulfrx.recipe.ui.screens.recipes.RecipesSearchViewModel import dev.ulfrx.recipe.ui.screens.recipes.RecipesSearchViewModel
import dev.ulfrx.recipe.ui.screens.recipes.RecipesViewModel import dev.ulfrx.recipe.ui.screens.recipes.RecipesViewModel
import dev.ulfrx.recipe.ui.screens.shopping.ShoppingSearchViewModel
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
import org.koin.plugin.module.dsl.viewModel import org.koin.plugin.module.dsl.viewModel
@@ -20,9 +22,11 @@ val shellModule =
viewModel<PantryViewModel>() viewModel<PantryViewModel>()
viewModel<ShoppingViewModel>() viewModel<ShoppingViewModel>()
// Per-tab Search ViewModels — pure echo this phase; Phase 5 / 8 inject // Per-tab Search ViewModels — pure echo this phase; Phase 5 / 6 / 8 / 9
// their respective SearchSource implementations. Both implement // inject their respective SearchSource implementations. All implement
// SearchControls so the shared ProvideSearchChrome composable drives them. // SearchControls so the shared ProvideSearchChrome composable drives them.
viewModel<RecipesSearchViewModel>() viewModel<RecipesSearchViewModel>()
viewModel<PantrySearchViewModel>() viewModel<PantrySearchViewModel>()
viewModel<PlannerSearchViewModel>()
viewModel<ShoppingSearchViewModel>()
} }

View File

@@ -14,11 +14,13 @@ import dev.ulfrx.recipe.ui.screens.pantry.PantryScreen
import dev.ulfrx.recipe.ui.screens.pantry.PantrySearchViewModel import dev.ulfrx.recipe.ui.screens.pantry.PantrySearchViewModel
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.PlannerSearchViewModel
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.RecipesScreen
import dev.ulfrx.recipe.ui.screens.recipes.RecipesSearchViewModel import dev.ulfrx.recipe.ui.screens.recipes.RecipesSearchViewModel
import dev.ulfrx.recipe.ui.screens.recipes.RecipesViewModel 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.ShoppingSearchViewModel
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,7 +74,8 @@ fun RootNavDisplay(
entryProvider = entryProvider { entryProvider = entryProvider {
entry<Screen.Planner.Home> { entry<Screen.Planner.Home> {
val vm: PlannerViewModel = koinViewModel() val vm: PlannerViewModel = koinViewModel()
PlannerScreen(viewModel = vm) val searchVm: PlannerSearchViewModel = koinViewModel()
PlannerScreen(viewModel = vm, searchViewModel = searchVm)
} }
entry<Screen.Recipes.Home> { entry<Screen.Recipes.Home> {
val vm: RecipesViewModel = koinViewModel() val vm: RecipesViewModel = koinViewModel()
@@ -86,7 +89,8 @@ fun RootNavDisplay(
} }
entry<Screen.Shopping.Home> { entry<Screen.Shopping.Home> {
val vm: ShoppingViewModel = koinViewModel() val vm: ShoppingViewModel = koinViewModel()
ShoppingScreen(viewModel = vm) val searchVm: ShoppingSearchViewModel = koinViewModel()
ShoppingScreen(viewModel = vm, searchViewModel = searchVm)
} }
}, },
) )

View File

@@ -16,22 +16,36 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.ulfrx.recipe.navigation.BottomBarDestination import dev.ulfrx.recipe.navigation.BottomBarDestination
import dev.ulfrx.recipe.ui.components.empty.EmptyState import dev.ulfrx.recipe.ui.components.empty.EmptyState
import dev.ulfrx.recipe.ui.components.search.ProvideSearchChrome
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_planner_subtitle import recipe.composeapp.generated.resources.empty_planner_subtitle
import recipe.composeapp.generated.resources.empty_planner_title import recipe.composeapp.generated.resources.empty_planner_title
import recipe.composeapp.generated.resources.search_placeholder_planner
import recipe.composeapp.generated.resources.shell_tab_planner import recipe.composeapp.generated.resources.shell_tab_planner
/** /**
* Phase 2.1 — empty-state screen for the Planner tab. Phase 6 replaces the * Phase 2.1 — empty-state screen for the Planner tab. Phase 6 replaces the
* empty body with the calendar grid. * empty body with the calendar grid.
*
* Owns its own bottom-bar chrome via [ProvideSearchChrome] — search affordance
* is shell-wide for visual consistency across tabs.
*/ */
@Composable @Composable
fun PlannerScreen(viewModel: PlannerViewModel) { fun PlannerScreen(
viewModel: PlannerViewModel,
searchViewModel: PlannerSearchViewModel,
) {
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
val state by viewModel.state.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle()
ProvideSearchChrome(
controls = searchViewModel,
placeholder = Res.string.search_placeholder_planner,
activeTab = BottomBarDestination.Planner,
)
Box( Box(
modifier = modifier =
Modifier Modifier

View File

@@ -0,0 +1,41 @@
package dev.ulfrx.recipe.ui.screens.planner
import androidx.lifecycle.ViewModel
import dev.ulfrx.recipe.ui.components.search.SearchControls
import dev.ulfrx.recipe.ui.components.search.SearchSource
import dev.ulfrx.recipe.ui.components.search.SearchState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
/**
* PlannerSearchViewModel — semantic parity with the Recipes / Pantry search VMs.
* Pure echo this phase; Phase 6/7 injects a Planner-specific SearchSource.
*/
class PlannerSearchViewModel(
@Suppress("UNUSED_PARAMETER")
private val searchSource: SearchSource? = null,
) : ViewModel(),
SearchControls {
private val _state = MutableStateFlow(SearchState())
override val state: StateFlow<SearchState> = _state.asStateFlow()
override fun open() {
_state.update { it.copy(isOpen = true) }
}
/** D-08: closing clears the query. */
override fun close() {
_state.value = SearchState(isOpen = false, query = "")
}
override fun onQueryChange(q: String) {
_state.update { it.copy(query = q) }
}
/** D-07: clear() resets only the query, preserves isOpen. */
override fun clear() {
_state.update { it.copy(query = "") }
}
}

View File

@@ -16,22 +16,36 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.ulfrx.recipe.navigation.BottomBarDestination import dev.ulfrx.recipe.navigation.BottomBarDestination
import dev.ulfrx.recipe.ui.components.empty.EmptyState import dev.ulfrx.recipe.ui.components.empty.EmptyState
import dev.ulfrx.recipe.ui.components.search.ProvideSearchChrome
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_shopping_subtitle import recipe.composeapp.generated.resources.empty_shopping_subtitle
import recipe.composeapp.generated.resources.empty_shopping_title import recipe.composeapp.generated.resources.empty_shopping_title
import recipe.composeapp.generated.resources.search_placeholder_shopping
import recipe.composeapp.generated.resources.shell_tab_shopping import recipe.composeapp.generated.resources.shell_tab_shopping
/** /**
* Phase 2.1 — empty-state screen for the Shopping tab. Phase 9 replaces the * Phase 2.1 — empty-state screen for the Shopping tab. Phase 9 replaces the
* empty body with the shopping list + session UI. * empty body with the shopping list + session UI.
*
* Owns its own bottom-bar chrome via [ProvideSearchChrome] — search affordance
* is shell-wide for visual consistency across tabs.
*/ */
@Composable @Composable
fun ShoppingScreen(viewModel: ShoppingViewModel) { fun ShoppingScreen(
viewModel: ShoppingViewModel,
searchViewModel: ShoppingSearchViewModel,
) {
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
val state by viewModel.state.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle()
ProvideSearchChrome(
controls = searchViewModel,
placeholder = Res.string.search_placeholder_shopping,
activeTab = BottomBarDestination.Shopping,
)
Box( Box(
modifier = Modifier.fillMaxSize().background(RecipeTheme.colors.background), modifier = Modifier.fillMaxSize().background(RecipeTheme.colors.background),
) { ) {

View File

@@ -0,0 +1,41 @@
package dev.ulfrx.recipe.ui.screens.shopping
import androidx.lifecycle.ViewModel
import dev.ulfrx.recipe.ui.components.search.SearchControls
import dev.ulfrx.recipe.ui.components.search.SearchSource
import dev.ulfrx.recipe.ui.components.search.SearchState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
/**
* ShoppingSearchViewModel — semantic parity with the Recipes / Pantry search VMs.
* Pure echo this phase; Phase 9 injects a Shopping-specific SearchSource.
*/
class ShoppingSearchViewModel(
@Suppress("UNUSED_PARAMETER")
private val searchSource: SearchSource? = null,
) : ViewModel(),
SearchControls {
private val _state = MutableStateFlow(SearchState())
override val state: StateFlow<SearchState> = _state.asStateFlow()
override fun open() {
_state.update { it.copy(isOpen = true) }
}
/** D-08: closing clears the query. */
override fun close() {
_state.value = SearchState(isOpen = false, query = "")
}
override fun onQueryChange(q: String) {
_state.update { it.copy(query = q) }
}
/** D-07: clear() resets only the query, preserves isOpen. */
override fun clear() {
_state.update { it.copy(query = "") }
}
}