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:
Jeena 2026-10-12 09:47:18 +00:00
parent 3635a4ed74
commit 20e6b07107
2 changed files with 2 additions and 171 deletions

View file

@ -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,

View file

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