From 20e6b07107f5c348f127813896584699b08d6255 Mon Sep 17 00:00:00 2001 From: Jeena Date: Mon, 12 Oct 2026 09:47:18 +0000 Subject: [PATCH] subs: Show the subscription feed as one paged video grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../jeena/jtubetv/ui/screens/HomeScreen.kt | 3 +- .../jtubetv/ui/screens/SubscriptionsScreen.kt | 170 ------------------ 2 files changed, 2 insertions(+), 171 deletions(-) delete mode 100644 app/src/main/java/net/jeena/jtubetv/ui/screens/SubscriptionsScreen.kt diff --git a/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeScreen.kt b/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeScreen.kt index 4efb9a0..a6f5aad 100644 --- a/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeScreen.kt +++ b/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeScreen.kt @@ -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, diff --git a/app/src/main/java/net/jeena/jtubetv/ui/screens/SubscriptionsScreen.kt b/app/src/main/java/net/jeena/jtubetv/ui/screens/SubscriptionsScreen.kt deleted file mode 100644 index d6e7eff..0000000 --- a/app/src/main/java/net/jeena/jtubetv/ui/screens/SubscriptionsScreen.kt +++ /dev/null @@ -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() { - - override fun onChunkLoaded(chunk: List) { - ImagePrefetcher.videoThumbnails(chunk) - } - - override suspend fun fetch(start: Long, count: Int): Result> = - repository.subscriptionVideos(start, count).map { it.data } -} - -private data class SubGroup( - val channel: VideoChannelSummary, - val startIndex: Int, - val videos: List, -) - -private fun buildGroups(items: List): List { - val groups = mutableListOf() - 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 create(modelClass: Class): 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() - } - } - } - } - } -} \ No newline at end of file