loading: shimmer placeholders on every list screen
The placeholders only existed on the home rows; the shared list screens (My videos, Search, Subscriptions, History, channel pages) still used a full-screen spinner, and the Channels screen did too. All of them now show card-shaped placeholders: the in-grid initial pass keeps the page header and a two-row placeholder block, growing lists swap the always- visible bottom spinner chip for a placeholder row driven by the new loadingMore state on PagedListViewModel, and the channels screen shows stub header bars plus placeholder rows while it starts up. Closes #19
This commit is contained in:
parent
4156704ffe
commit
52a0ba8fb9
4 changed files with 65 additions and 18 deletions
|
|
@ -25,6 +25,10 @@ abstract class PagedListViewModel<T> : ViewModel() {
|
|||
var loadingInitial by mutableStateOf(true)
|
||||
private set
|
||||
|
||||
/** True while a follow-up page is being fetched — UI shows placeholders ( issue #19). */
|
||||
var loadingMore by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
/**
|
||||
* Show an instant (possibly stale) snapshot before the network load lands
|
||||
* (issue #9): no spinner, content paints immediately and [refresh]-style
|
||||
|
|
@ -57,6 +61,7 @@ abstract class PagedListViewModel<T> : ViewModel() {
|
|||
fun loadMore(force: Boolean = false) {
|
||||
if (inFlight || (!force && exhausted)) return
|
||||
inFlight = true
|
||||
loadingMore = !force // the initial kick is a fresh list, not a "load more"
|
||||
initialError = null
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
|
|
@ -78,6 +83,7 @@ abstract class PagedListViewModel<T> : ViewModel() {
|
|||
)
|
||||
} finally {
|
||||
inFlight = false
|
||||
loadingMore = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
|
|
@ -340,9 +341,23 @@ fun ChannelsScreen(
|
|||
|
||||
|
||||
when {
|
||||
viewModel.loading && viewModel.userRows.isEmpty() -> Box(
|
||||
Modifier.fillMaxSize(), contentAlignment = Alignment.Center,
|
||||
) { androidx.compose.material3.CircularProgressIndicator() }
|
||||
viewModel.loading && viewModel.userRows.isEmpty() ->
|
||||
// Placeholder rows while the channels load (issue #19): header stub
|
||||
// + a row of shimmer card-shaped placeholders, twice.
|
||||
androidx.compose.foundation.layout.Column {
|
||||
repeat(2) {
|
||||
Box(Modifier.padding(start = 16.dp, top = 12.dp, bottom = 4.dp)) {
|
||||
Box(
|
||||
Modifier
|
||||
.width(180.dp)
|
||||
.height(22.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF232323)),
|
||||
)
|
||||
}
|
||||
ShimmerRow()
|
||||
}
|
||||
}
|
||||
viewModel.error != null && viewModel.userRows.isEmpty() ->
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import net.jeena.jtubetv.data.PeerTubeRepository
|
|||
import net.jeena.jtubetv.data.api.VideoDto
|
||||
import net.jeena.jtubetv.ui.list.PagedListViewModel
|
||||
import net.jeena.jtubetv.ui.list.Snapshots
|
||||
import net.jeena.jtubetv.ui.screens.list.ShimmerCard
|
||||
import net.jeena.jtubetv.ui.screens.list.VideoCard
|
||||
|
||||
/** Compact SmartTube-style card: ~4 and a bit visible per full-width row. */
|
||||
|
|
@ -407,12 +408,10 @@ fun HomeRowsScreen(
|
|||
|
||||
/** Placeholder row of dummy cards (issue #19). */
|
||||
@Composable
|
||||
private fun ShimmerRow(size: Int = 4) {
|
||||
fun ShimmerRow(size: Int = 4) {
|
||||
Row(Modifier.padding(horizontal = 12.dp)) {
|
||||
androidx.compose.foundation.layout.Row {
|
||||
}
|
||||
repeat(size) {
|
||||
net.jeena.jtubetv.ui.screens.list.ShimmerCard(
|
||||
ShimmerCard(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.width(rowCardWidth()),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
|
|
@ -117,13 +118,7 @@ fun VideoListScreen(
|
|||
}
|
||||
}
|
||||
|
||||
if (viewModel.loadingInitial && items.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
LaunchedEffect(source) { viewModel.loadMore(force = true) }
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (items.isEmpty()) {
|
||||
Box(
|
||||
|
|
@ -171,6 +166,30 @@ fun VideoListScreen(
|
|||
else -> {}
|
||||
}
|
||||
}
|
||||
if (items.isEmpty() && viewModel.loadingInitial) {
|
||||
// Placeholder cards while the page's first chunk loads (issue #19):
|
||||
// shaped like the real cards, with the faint shimmer, on every
|
||||
// VideoListScreen (My videos, Search, Subscriptions, History, channel pages).
|
||||
item(key = "initial-placeholders", span = { androidx.compose.foundation.lazy.grid.GridItemSpan(maxLineSpan) }) {
|
||||
LaunchedEffect(source) { viewModel.loadMore(force = true) }
|
||||
androidx.compose.foundation.layout.Column {
|
||||
repeat(2) {
|
||||
androidx.compose.foundation.layout.Row {
|
||||
repeat(cardsPerRow) {
|
||||
ShimmerCard(
|
||||
modifier = Modifier
|
||||
.padding(6.dp)
|
||||
.width(rowCardWidth()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (items.isEmpty() && !viewModel.loadingInitial && viewModel.initialError == null) {
|
||||
return@LazyVerticalGrid
|
||||
}
|
||||
itemsIndexed(items, key = { _, item -> itemStableKey(item) }) { index, item ->
|
||||
val video: VideoDto = when (item) {
|
||||
is VideoListItem.HistoryEntry -> item.history.video
|
||||
|
|
@ -193,10 +212,18 @@ fun VideoListScreen(
|
|||
LaunchedEffect(index) { viewModel.loadMore() }
|
||||
}
|
||||
}
|
||||
if (!viewModel.exhausted) {
|
||||
item {
|
||||
Box(Modifier.fillMaxWidth().padding(24.dp), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
if (viewModel.loadingMore) {
|
||||
// Growing the list: placeholder row (issue #19) instead of the
|
||||
// permanent spinner chip that used to sit under every list.
|
||||
item(key = "load-more-placeholders") {
|
||||
androidx.compose.foundation.layout.Row {
|
||||
repeat(cardsPerRow) {
|
||||
ShimmerCard(
|
||||
modifier = Modifier
|
||||
.padding(6.dp)
|
||||
.width(rowCardWidth()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue