From a8627211a3ecc567c191835249774db547e1b3c7 Mon Sep 17 00:00:00 2001 From: KhooLy <73142442+KhooLy@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:42:24 +0300 Subject: [PATCH] Add shimmer animation to skeletons and wire up the unused home disk cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HomeCategoryCache.load() has existed since before this migration but was never called anywhere — the ViewModel only ever wrote to it (setCategoriesAndCache -> homeCategoryCache.save), so cold app starts always waited on a full network fetch with nothing to show. Home now seeds _categories from the cached snapshot synchronously in loadInitialData before the network fetch starts, so a relaunch shows last-known content immediately while the real refresh happens in the background, same as the isLoading/rows.isEmpty() gating already in place downstream. Skeleton placeholders (Home, Search, Discover) were static gray boxes. Added Modifier.skeletonShimmer(), a small infinite-transition color pulse between FluxaColors.surfaceCard and surfaceRaised, applied to all three. --- .../com/fluxa/app/ui/catalog/HomeViewModel.kt | 28 ++++++++++++++-- .../kotlin/com/fluxa/app/shared/FluxaApp.kt | 6 ++-- .../com/fluxa/app/shared/SkeletonBox.kt | 32 +++++++++++++++++++ .../shared/feature/discover/DiscoverScreen.kt | 3 +- .../app/shared/feature/search/SearchScreen.kt | 3 +- 5 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 shared/src/commonMain/kotlin/com/fluxa/app/shared/SkeletonBox.kt 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() ) } }