mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-06 16:59:44 +00:00
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:
parent
c640ebc458
commit
a8627211a3
5 changed files with 65 additions and 7 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue