subs: Show the subscription feed as one paged video grid
Jeena wanted subscriptions to behave exactly like My videos — a single plain video list paged on scroll — instead of the SmartTube-style per-channel rows, which felt noisy when many channels are subscribed. The plain grid path already existed in VideoListScreen, so switch the Subscriptions destination over to it and drop the rows screen. Refs #5
This commit is contained in:
parent
3635a4ed74
commit
20e6b07107
2 changed files with 2 additions and 171 deletions
|
|
@ -187,7 +187,8 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
|
|||
upToTopBar = { runCatching { searchFocus.requestFocus() } },
|
||||
)
|
||||
Destination.Subscriptions ->
|
||||
SubscriptionRowsScreen(
|
||||
VideoListScreen(
|
||||
source = ListSource.Subscriptions,
|
||||
onOpenVideo = { playingVideo = it.shortUUID },
|
||||
restoreToken = restoreToken,
|
||||
epoch = epoch,
|
||||
|
|
|
|||
|
|
@ -1,170 +0,0 @@
|
|||
package net.jeena.jtubetv.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
import net.jeena.jtubetv.AppGraph
|
||||
import net.jeena.jtubetv.data.api.Avatar
|
||||
import net.jeena.jtubetv.data.api.VideoChannelSummary
|
||||
import net.jeena.jtubetv.data.api.VideoDto
|
||||
import net.jeena.jtubetv.ui.list.ImagePrefetcher
|
||||
import net.jeena.jtubetv.ui.list.PagedListViewModel
|
||||
|
||||
/**
|
||||
* Subscriptions, SmartTube-style: the feed grouped by channel — one row per
|
||||
* channel, newest-first within the row, row header = channel avatar + name.
|
||||
* Card sizes match the Home rows exactly.
|
||||
*/
|
||||
class SubscriptionRowsViewModel(
|
||||
private val repository: net.jeena.jtubetv.data.PeerTubeRepository = AppGraph.repository,
|
||||
) : PagedListViewModel<VideoDto>() {
|
||||
|
||||
override fun onChunkLoaded(chunk: List<VideoDto>) {
|
||||
ImagePrefetcher.videoThumbnails(chunk)
|
||||
}
|
||||
|
||||
override suspend fun fetch(start: Long, count: Int): Result<List<VideoDto>> =
|
||||
repository.subscriptionVideos(start, count).map { it.data }
|
||||
}
|
||||
|
||||
private data class SubGroup(
|
||||
val channel: VideoChannelSummary,
|
||||
val startIndex: Int,
|
||||
val videos: List<VideoDto>,
|
||||
)
|
||||
|
||||
private fun buildGroups(items: List<VideoDto>): List<SubGroup> {
|
||||
val groups = mutableListOf<SubGroup>()
|
||||
items.forEachIndexed { index, video ->
|
||||
val last = groups.lastOrNull()
|
||||
val same = last != null &&
|
||||
last.channel.name == video.channel.name &&
|
||||
last.channel.host == video.channel.host
|
||||
if (same) {
|
||||
groups[groups.lastIndex] = last!!.copy(videos = last.videos + video)
|
||||
} else {
|
||||
groups += SubGroup(video.channel, index, listOf(video))
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SubscriptionRowsScreen(
|
||||
onOpenVideo: (VideoDto) -> Unit,
|
||||
restoreToken: Int = 0,
|
||||
epoch: Int = 0,
|
||||
onOpenDrawer: () -> Unit = {},
|
||||
upToTopBar: (() -> Unit)? = null,
|
||||
viewModel: SubscriptionRowsViewModel = viewModel(
|
||||
key = "subsrows$epoch",
|
||||
factory = object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||
SubscriptionRowsViewModel() as T
|
||||
},
|
||||
),
|
||||
) {
|
||||
val items = viewModel.items
|
||||
LaunchedEffect(Unit) { if (items.isEmpty()) viewModel.loadMore(force = true) }
|
||||
LaunchedEffect(restoreToken) { if (restoreToken > 0) viewModel.refresh() }
|
||||
|
||||
if (items.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
if (viewModel.initialError != null) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Could not load subscriptions", color = MaterialTheme.colorScheme.error)
|
||||
Text(
|
||||
viewModel.initialError ?: "",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
androidx.compose.material3.Button(onClick = { viewModel.retryInitial() }) {
|
||||
Text("Retry")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
androidx.compose.material3.CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val groups = remember(items) { buildGroups(items) }
|
||||
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
groups.forEach { group ->
|
||||
item(key = "subrowhead" + group.channel.name + "@" + group.channel.host + group.startIndex) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
val avatar = group.channel.avatars.maxByOrNull { it.width }
|
||||
if (avatar != null) {
|
||||
AsyncImage(
|
||||
model = coil.request.ImageRequest.Builder(
|
||||
androidx.compose.ui.platform.LocalContext.current,
|
||||
)
|
||||
.data(avatar.fileUrl
|
||||
?: (AppGraph.tokenStore.baseUrl ?: "") + avatar.path)
|
||||
.size(80)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF222222)),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = group.channel.displayName.ifBlank { group.channel.name },
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
item(key = "subrow" + group.channel.name + "@" + group.channel.host + group.startIndex) {
|
||||
FocusScrollRow(
|
||||
videos = group.videos,
|
||||
progressOf = { null },
|
||||
onOpenVideo = onOpenVideo,
|
||||
onLeftAtStart = onOpenDrawer,
|
||||
upToTopBar = upToTopBar.takeIf { group == groups.firstOrNull() },
|
||||
) { rowIndex, _ ->
|
||||
if (group.startIndex + rowIndex >= items.size - PagedListViewModel.LOOKAHEAD) {
|
||||
viewModel.loadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue