channels: hide video-less channels, sort users by subscribers first

Video-less channels dropped out of the row build when the fast path
switched to the video listing, and the previous commit wrongly merged
the own empty channels back in — the user rule is the opposite: a
channel (or a whole user row) with no videos renders at all; watching-
only accounts must stay invisible. The own row only exists when the
logged-in user owns at least one channel with videos.

The ordering is now one rule on both levels — user rows by each row's
channel sums, channels inside a row: subscribers, videos, views (desc).
Settled order on the TV verified against the instance's real follower
counts (kaylee 9, love 9, georg 6, yingfen 5, kinga 1 — georg's 211
videos only tie-break), with the own row first.
This commit is contained in:
Jeena 2026-10-19 14:28:11 +00:00
parent c0a7e94b14
commit 182650414c

View file

@ -75,10 +75,10 @@ import net.jeena.jtubetv.ui.screens.list.dpadCenterActivatable
/**
* Channels, SmartTube-style: one row per LOCAL user.
*
* - Row 1: the logged-in user ("<display name>'s Channels"), then the other local
* users sorted by videos / subscribers / plays (desc).
* - Each row: that user's channels, same ordering; channels with no videos simply
* stay inside their user's row.
* - Row 1: the logged-in user ("<display name>'s Channels") — only when they own
* at least one channel WITH videos; then the other local users, rows and
* channels alike sorted by subscribers / videos / views (desc).
* - Channels with no videos never render (watching-only accounts), at any level.
* - Followed-instance channels at the end, grouped per platform.
*
* API limits (verified, PeerTube 8.3.1): the /video-channels and /accounts listings
@ -270,25 +270,20 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
}
private fun rebuildRowsInner(host: String?) {
val allLocals = allChannels.filter { it.host == host }
// The video-listing fast path cannot see channels with no videos at all —
// an account owning only such channels used to lose its row entirely
// (tvbot: the local tvbots channel exists, it just never uploaded). Merge
// the logged-in user's own local channels in; the group sort below still
// puts the own row first. The account-channels listing already carries
// the full card shape (avatar/banners/followers), no extra calls.
var locals = allLocals
if (userChannels.isNotEmpty()) {
val missing = userChannels
.filter { it.host == host && allLocals.none { l -> l.name == it.name } }
if (missing.isNotEmpty()) locals = allLocals + missing
}
// Channels without videos don't exist on this screen: plenty of local
// accounts are watching-only and their empty channels are pure noise if
// rendered (user rule). This also covers the own row — it only renders
// when the logged-in user has at least one channel with videos, and the
// group sort below still puts it first in that case.
val locals = allChannels
.filter { it.host == host && statOf(it) > 0 }
val ownNames = userChannels.map { c -> c.name }.toSet()
val byOwner = locals.groupBy { ownerKey(it) }
val groups = byOwner.values.map { row ->
val sorted = row.sortedWith(
compareByDescending<VideoChannelDto> { statOf(it) }
.thenByDescending { it.followersCount }
// One ordering everywhere: subscribers, videos, views (desc).
compareByDescending<VideoChannelDto> { it.followersCount }
.thenByDescending { statOf(it) }
.thenByDescending { playsOf(it) },
)
val own = row.firstOrNull { it.name in ownNames }
@ -301,8 +296,9 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
}
userRows = groups.sortedWith(
compareByDescending<UserGroup> { g -> g.channels.any { it.name in ownNames } }
.thenByDescending { g -> g.channels.sumOf { it.followersCount } }
.thenByDescending { g -> g.channels.sumOf { statsMap[it.name + "@" + it.host]?.videos ?: 0 } }
.thenByDescending { g -> g.channels.sumOf { it.followersCount } },
.thenByDescending { g -> g.channels.sumOf { statsMap[it.name + "@" + it.host]?.plays ?: 0 } },
)
Log.w(
"JTube",