Add shimmer animation to skeletons and wire up the unused home disk cache

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.
This commit is contained in:
KhooLy 2026-07-13 21:42:24 +03:00
parent c640ebc458
commit a8627211a3
5 changed files with 65 additions and 7 deletions

View file

@ -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<Boolean> = _isSearchLoading.asStateFlow()
private val loadMoreInFlight = mutableSetOf<String>()
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 {

View file

@ -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()
)
}
}

View file

@ -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)
}

View file

@ -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()
)
}
}

View file

@ -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()
)
}
}