diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt index 0ca06ae..01f2d32 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt @@ -244,6 +244,8 @@ class HomeViewModel @Inject constructor( private var traktWatchedState: TraktWatchedState = TraktWatchedState() private var currentActiveProfile: UserProfile? = null private var searchJob: Job? = null + private val _isSearchLoading = MutableStateFlow(false) + val isSearchLoading: StateFlow = _isSearchLoading.asStateFlow() private val loadMoreInFlight = mutableSetOf() private val playbackController by lazy { coordinatorFactory.playback( @@ -860,12 +862,14 @@ class HomeViewModel @Inject constructor( if (query.isEmpty()) { searchFocusState.searchResultsValue = emptyList() searchFocusState.searchRowsValue = emptyList() + _isSearchLoading.value = false return } searchJob = viewModelScope.launch { - delay(400) - if (!isActive) return@launch try { + _isSearchLoading.value = true + delay(400) + if (!isActive) return@launch val trimmedQuery = query.trim() val rows = addonRepository.searchRows( query = trimmedQuery, @@ -881,6 +885,8 @@ class HomeViewModel @Inject constructor( searchFocusState.searchResultsValue = allRows.flatMap { it.items }.distinctBy { it.id }.take(80) } catch (e: Exception) { e.printStackTrace() + } finally { + _isSearchLoading.value = false } } } @@ -894,6 +900,18 @@ class HomeViewModel @Inject constructor( searchFocusState.searchHistoryValue = updated } + fun recordSearchSelection(id: String, type: String) { + val selected = searchFocusState.searchResultsValue.firstOrNull { meta -> + meta.id == id && meta.type == type + } ?: searchFocusState.searchResultsValue.firstOrNull { it.id == id } + selected?.let(::addToSearchHistory) + } + + fun clearSearchHistory() { + searchHistoryStore.save(emptyList(), currentActiveProfile) + searchFocusState.searchHistoryValue = emptyList() + } + fun loadMore(categoryId: String) { val category = _categories.value.firstOrNull { it.id == categoryId } ?: _collectionFolderCategories.value[categoryId] @@ -1089,6 +1107,12 @@ class HomeViewModel @Inject constructor( savedTvFocusedRowIndex = -1 savedCategoryScrollPositions.clear() } + if (_categories.value.isEmpty()) { + val cached = homeCategoryCache.load(activeProfile) + if (cached.isNotEmpty()) { + setCategoriesState(cached) + } + } viewModelScope.launch { setLoadingState(true) try { 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 19d5a50..fb79b85 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaApp.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaApp.kt @@ -307,7 +307,7 @@ private fun FluxaHomeSkeleton(modifier: Modifier = Modifier) { modifier = Modifier .fillMaxWidth() .aspectRatio(3f / 4f) - .background(FluxaColors.surfaceCard) + .skeletonShimmer(shape = androidx.compose.ui.graphics.RectangleShape) ) } items(3, key = { "row-skeleton-$it" }) { @@ -317,7 +317,7 @@ private fun FluxaHomeSkeleton(modifier: Modifier = Modifier) { .padding(horizontal = 20.dp) .width(120.dp) .height(16.dp) - .background(FluxaColors.surfaceCard) + .skeletonShimmer() ) LazyRow( contentPadding = PaddingValues(horizontal = 20.dp), @@ -328,7 +328,7 @@ private fun FluxaHomeSkeleton(modifier: Modifier = Modifier) { modifier = Modifier .width(110.dp) .aspectRatio(2f / 3f) - .background(FluxaColors.surfaceCard) + .skeletonShimmer() ) } } diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/SkeletonBox.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/SkeletonBox.kt new file mode 100644 index 0000000..e162ce9 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/SkeletonBox.kt @@ -0,0 +1,32 @@ +package com.fluxa.app.shared + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.unit.dp +import com.fluxa.app.ui.catalog.FluxaColors + +@Composable +fun Modifier.skeletonShimmer(shape: Shape = RoundedCornerShape(10.dp)): Modifier { + val transition = rememberInfiniteTransition(label = "skeleton") + val fraction by transition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 900, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "skeletonFraction" + ) + return this.background(lerp(FluxaColors.surfaceCard, FluxaColors.surfaceRaised, fraction), shape) +} 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 index 1c49a8d..16ff95e 100644 --- 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 @@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp import com.fluxa.app.common.AppStrings import com.fluxa.app.shared.feature.catalog.CatalogItemUiModel import com.fluxa.app.shared.feature.catalog.CatalogRowUiModel +import com.fluxa.app.shared.skeletonShimmer import com.fluxa.app.shared.feature.search.SearchResultRows import com.fluxa.app.shared.feature.search.SearchResults import com.fluxa.app.ui.catalog.CatalogCard @@ -131,7 +132,7 @@ private fun DiscoverSkeletonGrid(modifier: Modifier = Modifier) { modifier = Modifier .fillMaxWidth() .aspectRatio(2f / 3f) - .background(FluxaColors.surfaceCard, RoundedCornerShape(10.dp)) + .skeletonShimmer() ) } } diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/search/SearchScreen.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/search/SearchScreen.kt index 548eda0..7240157 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/search/SearchScreen.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/search/SearchScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.foundation.text.KeyboardOptions import com.fluxa.app.common.AppStrings import com.fluxa.app.shared.feature.catalog.CatalogItemUiModel import com.fluxa.app.shared.feature.catalog.CatalogRowUiModel +import com.fluxa.app.shared.skeletonShimmer import com.fluxa.app.ui.catalog.CatalogCard import com.fluxa.app.ui.catalog.FluxaColors @@ -210,7 +211,7 @@ private fun SearchSkeletonGrid(modifier: Modifier) { modifier = Modifier .fillMaxWidth() .aspectRatio(2f / 3f) - .background(FluxaColors.surfaceCard, androidx.compose.foundation.shape.RoundedCornerShape(10.dp)) + .skeletonShimmer() ) } }