diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaApp.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaApp.kt index b07d8a4..d1ead5f 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaApp.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaApp.kt @@ -27,6 +27,9 @@ import com.fluxa.app.shared.feature.catalog.CatalogItemUiModel import com.fluxa.app.shared.feature.detail.DetailAction import com.fluxa.app.shared.feature.detail.DetailScreen import com.fluxa.app.shared.feature.detail.DetailUiState +import com.fluxa.app.shared.feature.discover.DiscoverAction +import com.fluxa.app.shared.feature.discover.DiscoverScreen +import com.fluxa.app.shared.feature.discover.DiscoverUiState import com.fluxa.app.shared.feature.search.SearchAction import com.fluxa.app.shared.feature.search.SearchScreen import com.fluxa.app.shared.feature.search.SearchUiState @@ -58,6 +61,8 @@ fun FluxaApp( onDetailAction: (DetailAction) -> Unit = {}, searchState: SearchUiState? = null, onSearchAction: (SearchAction) -> Unit = {}, + discoverState: DiscoverUiState? = null, + onDiscoverAction: (DiscoverAction) -> Unit = {}, modifier: Modifier = Modifier ) { MaterialTheme { @@ -85,6 +90,13 @@ fun FluxaApp( onItemSelected = { item -> onSearchAction(SearchAction.ItemSelected(item)) }, modifier = Modifier.weight(1f) ) + state.destination == FluxaDestination.Discover && discoverState != null -> DiscoverScreen( + state = discoverState, + language = state.language, + onFiltersChanged = { filters -> onDiscoverAction(DiscoverAction.FiltersChanged(filters)) }, + onItemSelected = { item -> onDiscoverAction(DiscoverAction.ItemSelected(item)) }, + modifier = Modifier.weight(1f) + ) state.destination == FluxaDestination.Home -> FluxaHomeContent( state = state, onCatalogAction = onCatalogAction, diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaAppHost.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaAppHost.kt index 775438c..c485c6c 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaAppHost.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaAppHost.kt @@ -13,10 +13,14 @@ import com.fluxa.app.shared.feature.catalog.CatalogHomeStore import com.fluxa.app.shared.feature.detail.DetailAction import com.fluxa.app.shared.feature.detail.DetailDataSource import com.fluxa.app.shared.feature.detail.DetailStore +import com.fluxa.app.shared.feature.discover.DiscoverAction +import com.fluxa.app.shared.feature.discover.DiscoverDataSource +import com.fluxa.app.shared.feature.discover.DiscoverStore import com.fluxa.app.shared.feature.search.SearchAction import com.fluxa.app.shared.feature.search.SearchDataSource import com.fluxa.app.shared.feature.search.SearchStore import com.fluxa.app.shared.platform.FluxaDetailServices +import com.fluxa.app.shared.platform.FluxaDiscoverServices import com.fluxa.app.shared.platform.FluxaPlatformServices import com.fluxa.app.shared.platform.FluxaSearchServices import kotlinx.coroutines.launch @@ -31,6 +35,7 @@ fun FluxaAppHost( FluxaAppHost( catalogHomeDataSource = platformServices.catalogHomeDataSource, detailDataSource = (platformServices as? FluxaDetailServices)?.detailDataSource, + discoverDataSource = (platformServices as? FluxaDiscoverServices)?.discoverDataSource, searchDataSource = (platformServices as? FluxaSearchServices)?.searchDataSource, language = language, onCatalogAction = onCatalogAction, @@ -42,6 +47,7 @@ fun FluxaAppHost( fun FluxaAppHost( catalogHomeDataSource: CatalogHomeDataSource, detailDataSource: DetailDataSource? = null, + discoverDataSource: DiscoverDataSource? = null, searchDataSource: SearchDataSource? = null, language: String? = null, onCatalogAction: (CatalogAction) -> Unit = {}, @@ -56,6 +62,10 @@ fun FluxaAppHost( remember(source) { SearchStore(source, scope) } } val searchState = searchStore?.state?.collectAsState()?.value + val discoverStore = discoverDataSource?.let { source -> + remember(source) { DiscoverStore(source, scope) } + } + val discoverState = discoverStore?.state?.collectAsState()?.value val appState = rememberFluxaAppState() val selectedDetail = appState.uiState.selectedDetail val detailStore = selectedDetail?.let { item -> @@ -112,6 +122,16 @@ fun FluxaAppHost( searchStore?.dispatch(action) } }, + discoverState = discoverState, + onDiscoverAction = { action -> + if (action is DiscoverAction.ItemSelected) { + appState.selectDetail(action.item) + onCatalogAction(CatalogAction.ItemSelected(action.item)) + } + scope.launch { + discoverStore?.dispatch(action) + } + }, modifier = modifier ) } diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/discover/DiscoverScreen.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/discover/DiscoverScreen.kt new file mode 100644 index 0000000..21fc3ed --- /dev/null +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/discover/DiscoverScreen.kt @@ -0,0 +1,137 @@ +package com.fluxa.app.shared.feature.discover + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.fluxa.app.common.AppStrings +import com.fluxa.app.shared.feature.catalog.CatalogItemUiModel +import com.fluxa.app.ui.catalog.CatalogCard +import com.fluxa.app.ui.catalog.FluxaColors + +@Composable +fun DiscoverScreen( + state: DiscoverUiState, + language: String?, + onFiltersChanged: (DiscoverFiltersUiModel) -> Unit, + onItemSelected: (CatalogItemUiModel) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .background(FluxaColors.background) + .padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = AppStrings.t(language, "nav.discover"), + color = Color.White, + fontWeight = FontWeight.Bold + ) + DiscoverFilters( + filters = state.filters, + catalogOptions = state.catalogOptions, + genreOptions = state.genreOptions, + language = language, + onFiltersChanged = onFiltersChanged + ) + when { + state.isLoading && state.results.isEmpty() -> Box( + modifier = Modifier.weight(1f).fillMaxWidth(), + contentAlignment = Alignment.Center + ) { CircularProgressIndicator(color = Color.White) } + state.results.isEmpty() -> Box( + modifier = Modifier.weight(1f).fillMaxWidth(), + contentAlignment = Alignment.Center + ) { Text(AppStrings.t(language, "auto.no_results_yet"), color = Color.White) } + else -> LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 128.dp), + modifier = Modifier.weight(1f).fillMaxWidth(), + contentPadding = PaddingValues(bottom = 20.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(state.results, key = { "${it.type}:${it.id}" }) { item -> + CatalogCard(model = item.card, onClick = { onItemSelected(item) }) + } + } + } + } +} + +@Composable +private fun DiscoverFilters( + filters: DiscoverFiltersUiModel, + catalogOptions: List, + genreOptions: List, + language: String?, + onFiltersChanged: (DiscoverFiltersUiModel) -> Unit +) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + DiscoverFilterRow( + options = listOf( + DiscoverFilterOptionUiModel("movie", AppStrings.t(language, "auto.movie")), + DiscoverFilterOptionUiModel("series", AppStrings.t(language, "auto.series")) + ), + selectedId = filters.contentType, + onSelected = { value -> + onFiltersChanged(DiscoverFiltersUiModel(contentType = value.orEmpty())) + } + ) + if (catalogOptions.isNotEmpty()) { + DiscoverFilterRow( + options = catalogOptions, + selectedId = filters.catalogKey, + onSelected = { value -> + onFiltersChanged(filters.copy(catalogKey = value, genre = null)) + } + ) + } + if (genreOptions.isNotEmpty()) { + DiscoverFilterRow( + options = genreOptions, + selectedId = filters.genre, + onSelected = { value -> + onFiltersChanged(filters.copy(genre = value)) + } + ) + } + } +} + +@Composable +private fun DiscoverFilterRow( + options: List, + selectedId: String?, + onSelected: (String?) -> Unit +) { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + options.forEach { option -> + val selected = option.id == selectedId + Text( + text = option.label, + color = if (selected) Color.White else Color.White.copy(alpha = 0.6f), + fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium, + modifier = Modifier.clickable { onSelected(option.id) } + ) + } + } +}