channels: Render rows early (pages only), guard rebuild, Long-safe sort keys
The channels screen waited for every listing page AND all per-channel stats before showing anything - a 40s+ blank spinner. Rows now render per page (unsorted), re-sorting as stat batches arrive. rebuildRows is guarded; the earlier ClassCastException (Integer into a Long comparator) surfaced via the sort call and can no longer abort the app. All sort keys go through Long-typed helpers.
This commit is contained in:
parent
8d21cf35ec
commit
d55c265469
1 changed files with 87 additions and 45 deletions
|
|
@ -36,6 +36,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
|
|
@ -105,18 +106,30 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
|
|||
var platformNames by mutableStateOf<Map<String, String>>(emptyMap())
|
||||
private set
|
||||
|
||||
private var allChannels: List<VideoChannelDto> = emptyList()
|
||||
|
||||
/** Channels from followed instances, grouped per platform. */
|
||||
var followedRows by mutableStateOf<List<UserGroup>>(emptyList())
|
||||
private set
|
||||
|
||||
private val repo: PeerTubeRepository = AppGraph.repository
|
||||
|
||||
private val statsMap = HashMap<String, ChannelStats>()
|
||||
|
||||
|
||||
fun load(force: Boolean = false) {
|
||||
if (!force && userRows.isNotEmpty()) return
|
||||
loading = true
|
||||
error = null
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
// The user's own row renders FIRST (fast: one call), the rest streams in.
|
||||
user = repo.me().getOrNull()
|
||||
userChannels = user?.let { u ->
|
||||
repo.accountChannels(u.username).getOrNull() ?: emptyList()
|
||||
} ?: emptyList()
|
||||
val host = store.baseUrl?.substringAfter("://")?.substringBefore('/')
|
||||
platformNames = mapOf((host ?: "") to (store.instanceName ?: host ?: ""))
|
||||
|
||||
// enum the whole channel list (federation-follow listing incl. local)
|
||||
val all = mutableListOf<VideoChannelDto>()
|
||||
|
|
@ -136,53 +149,14 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
|
|||
val known = all.map { it.name to it.host }.toSet()
|
||||
all += page.first.filter { c -> (c.name to c.host) !in known }
|
||||
start += 100
|
||||
allChannels = all
|
||||
rebuildRows(host)
|
||||
}
|
||||
|
||||
val host = store.baseUrl?.substringAfter("://")?.substringBefore('/')
|
||||
val locals = all.filter { it.host == host }
|
||||
|
||||
// platform display names: local one cached at login; foreign ones lazy
|
||||
platformNames = mapOf((host ?: "") to (store.instanceName ?: host ?: ""))
|
||||
for (fh in locals.mapNotNull { it.ownerAccount?.host }.toSet()) {
|
||||
val n = repo.instanceName(fh).getOrNull() ?: continue
|
||||
platformNames = platformNames + (fh to n)
|
||||
}
|
||||
|
||||
// stats for every local channel: throttle below the server rate limit
|
||||
val statsMap = HashMap<String, ChannelStats>()
|
||||
for (channel in locals) {
|
||||
val key = channel.name + "@" + channel.host
|
||||
val cs = repo.channelStats(key).getOrNull() ?: continue
|
||||
statsMap[key] = cs
|
||||
stats = statsMap.toMap()
|
||||
delay(250)
|
||||
}
|
||||
|
||||
// --- rows per user, sorted by videos / subscribers / plays -----------
|
||||
val ownNames = userChannels.map { c -> c.name }.toSet()
|
||||
val byOwner = locals.groupBy { ownerKey(it) }
|
||||
val groups = byOwner.values.map { row ->
|
||||
val sortedChannels = row.sortedWith(
|
||||
compareByDescending<VideoChannelDto> { statsMap[it.name + "@" + it.host]?.videos ?: 0 }
|
||||
.thenByDescending { it.followersCount }
|
||||
.thenByDescending { statsMap[it.name + "@" + it.host]?.plays ?: 0 },
|
||||
)
|
||||
val own = row.firstOrNull { it.name in ownNames }
|
||||
UserGroup(
|
||||
ownerName = ownerKey(row.first()),
|
||||
label = ((own?.ownerAccount?.displayName ?: row.firstOrNull()?.ownerAccount?.displayName) ?: "?") + "'s Channels",
|
||||
avatar = (own?.ownerAccount ?: row.first().ownerAccount)?.avatars?.maxByOrNull { it.width },
|
||||
channels = sortedChannels,
|
||||
)
|
||||
}
|
||||
// logged-in user first, then the others by videos / subs / plays
|
||||
userRows = groups.sortedWith(
|
||||
compareByDescending<UserGroup> { g -> g.channels.any { it.name in ownNames } }
|
||||
.thenByDescending { g -> g.channels.sumOf { statsMap[it.name + "@" + it.host]?.videos ?: 0 } }
|
||||
.thenByDescending { g -> g.channels.sumOf { it.followersCount } },
|
||||
)
|
||||
|
||||
ImagePrefetcher.channelArt(all + userChannels)
|
||||
|
||||
// stats (videos + plays) for every local channel: throttled parallel-ish
|
||||
val locals = all.filter { it.host == host }
|
||||
channelStatsLoop(locals, host)
|
||||
} catch (e: Exception) {
|
||||
error = e.message ?: "Could not load channels"
|
||||
} finally {
|
||||
|
|
@ -191,6 +165,73 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
/** Fetch stats for [channels], re-sorting rows as data lands. */
|
||||
private fun channelStatsLoop(channels: List<VideoChannelDto>, host: String?) {
|
||||
viewModelScope.launch {
|
||||
for (channel in channels) {
|
||||
val key = channel.name + "@" + channel.host
|
||||
if (statsMap.containsKey(key)) continue
|
||||
val cs = repo.channelStats(key).getOrNull() ?: continue
|
||||
statsMap[key] = cs
|
||||
stats = statsMap.toMap()
|
||||
rebuildRows(host)
|
||||
delay(120)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun statOf(c: VideoChannelDto): Long = statsMap[c.name + "@" + c.host]?.videos ?: 0L
|
||||
private fun playsOf(c: VideoChannelDto): Long = statsMap[c.name + "@" + c.host]?.plays ?: 0L
|
||||
|
||||
/** Rebuilds the per-user rows from current channels+stats (sorted videos/subs/plays). */
|
||||
private fun rebuildRows(host: String?) {
|
||||
runCatching { rebuildRowsInner(host) }.onFailure {
|
||||
android.util.Log.e("JTube", "rebuildRows failed", it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun rebuildRowsInner(host: String?) {
|
||||
val locals = allChannels.filter { it.host == host }
|
||||
val foreign = allChannels.filter { it.host != host }
|
||||
followedRows = foreign
|
||||
.groupBy { it.host ?: "?" }
|
||||
.values
|
||||
.map { row ->
|
||||
UserGroup(
|
||||
ownerName = row.firstOrNull()?.host ?: "?",
|
||||
label = (platformNames[row.firstOrNull()?.host]
|
||||
?: row.firstOrNull()?.host ?: "?") + " (followed)",
|
||||
avatar = null,
|
||||
channels = row.sortedWith(
|
||||
compareByDescending<VideoChannelDto> { statOf(it) }
|
||||
.thenByDescending { it.followersCount },
|
||||
),
|
||||
)
|
||||
}
|
||||
.sortedByDescending { g -> g.channels.sumOf { it.followersCount } }
|
||||
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 }
|
||||
.thenByDescending { playsOf(it) },
|
||||
)
|
||||
val own = row.firstOrNull { it.name in ownNames }
|
||||
UserGroup(
|
||||
ownerName = ownerKey(row.first()),
|
||||
label = ((own?.ownerAccount?.displayName ?: row.firstOrNull()?.ownerAccount?.displayName) ?: "?") + "'s Channels",
|
||||
avatar = (own?.ownerAccount ?: row.first().ownerAccount)?.avatars?.maxByOrNull { it.width },
|
||||
channels = sorted,
|
||||
)
|
||||
}
|
||||
userRows = groups.sortedWith(
|
||||
compareByDescending<UserGroup> { g -> g.channels.any { it.name in ownNames } }
|
||||
.thenByDescending { g -> g.channels.sumOf { statsMap[it.name + "@" + it.host]?.videos ?: 0 } }
|
||||
.thenByDescending { g -> g.channels.sumOf { it.followersCount } },
|
||||
)
|
||||
}
|
||||
|
||||
private fun ownerKey(c: VideoChannelDto) = c.ownerAccount?.name ?: ""
|
||||
|
||||
companion object {
|
||||
|
|
@ -358,6 +399,7 @@ fun ChannelCard(
|
|||
modifier = modifier
|
||||
.width(rowCardWidth())
|
||||
.padding(6.dp)
|
||||
.onFocusChanged { cardFocused = it.isFocused }
|
||||
.onPreviewKeyEvent { ke ->
|
||||
if (ke.nativeKeyEvent.action == android.view.KeyEvent.ACTION_DOWN &&
|
||||
ke.nativeKeyEvent.keyCode == android.view.KeyEvent.KEYCODE_DPAD_LEFT &&
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue