vc=86: audit-fix sprint (HIGH H2 + 5 MED/LOW from the 2026-06-21 audit)
- Audio-only toggle no longer drops the max-resolution cap: both the fullscreen button (PlayerScreen) and the detail Audio pill (VideoDetailBody) rebuilt TrackSelectionParameters from a fresh Builder, wiping the data-saver ceiling. Now buildUpon() the existing params so the cap survives. (H2) - Subscriptions Refresh button no longer sticks at "..." forever on a warm restart within the cache TTL: refreshIfStale clears the initial loading seed when it decides nothing needs refreshing. (M2) - Search + Channel result lists get a stable item key (video url) so paging / shorts-filtering stops re-binding rows to new data (re-triggered thumbnail loads, scroll shift). (M3, M4) - IosSafeHttpDataSource: the unknown-length (LENGTH_UNSET) chunk path rolls forward to the next Range chunk at inner-EOF instead of re-reading the exhausted source forever (was truncating playback to the first chunk). (M5) - strawcore channel_feed_rss propagates the real failure (network/HTTP/parse) instead of collapsing every error to an empty list, so a broken fetch is distinguishable from "no new videos" (subscription_feed keeps its per-channel tolerance for fan-out). (M6) - Feed recency: a clock-skewed future upload emits "0 seconds ago" (parses to top) instead of "just now" (which Kotlin's recency parser couldn't read, so the item sank to the bottom). (L4) Deferred to a follow-up: M1 (bg-refresh cache-key mismatch — needs a worker redesign) + M7 (build config-cache wiring). Verified: cargo check/clippy + full Android compileDebugKotlin green.
This commit is contained in:
parent
055c9c6d4f
commit
791975ca4a
8 changed files with 89 additions and 17 deletions
|
|
@ -184,7 +184,7 @@ fun ChannelScreen(
|
|||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
items(filteredVideos) { item ->
|
||||
items(filteredVideos, key = { it.url }) { item ->
|
||||
ChannelVideoRow(
|
||||
item = item,
|
||||
onClick = { onOpenVideo(item.url, item.title) },
|
||||
|
|
|
|||
|
|
@ -327,7 +327,10 @@ fun VideoDetailBody(
|
|||
uploaderUrl = d.uploaderUrl,
|
||||
)
|
||||
}
|
||||
c.trackSelectionParameters = TrackSelectionParameters.Builder(context)
|
||||
// buildUpon() preserves the existing
|
||||
// max-resolution cap; a fresh Builder
|
||||
// would wipe it (data-saver leak).
|
||||
c.trackSelectionParameters = c.trackSelectionParameters.buildUpon()
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true)
|
||||
.build()
|
||||
if (!c.isPlaying) c.play()
|
||||
|
|
|
|||
|
|
@ -163,7 +163,16 @@ class SubscriptionFeedViewModel : ViewModel() {
|
|||
val entry = channelCache[ch.url]
|
||||
entry == null || now - entry.fetchedAt >= perChannelTtlMs
|
||||
}
|
||||
if (anyStale || _ui.value.items.isEmpty()) refreshInternal(force = false)
|
||||
if (anyStale || _ui.value.items.isEmpty()) {
|
||||
refreshInternal(force = false)
|
||||
} else {
|
||||
// Everything is fresh and already on screen (warm restart within
|
||||
// the per-channel TTL) — clear the initial loading=true seed so
|
||||
// the Refresh button doesn't sit at "..." forever. The
|
||||
// full-screen spinner is gated on empty items so it never showed;
|
||||
// only StrawHome's button label ("..." vs "Refresh") was stuck.
|
||||
_ui.update { it.copy(loading = false) }
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() = refreshInternal(force = true)
|
||||
|
|
|
|||
|
|
@ -234,7 +234,13 @@ fun PlayerScreen(
|
|||
desc = if (audioOnly) "Audio-only on" else "Video on",
|
||||
) {
|
||||
audioOnly = !audioOnly
|
||||
controller.trackSelectionParameters = TrackSelectionParameters.Builder(context)
|
||||
// buildUpon(), NOT a fresh Builder(context) — a new
|
||||
// builder discards every existing constraint, most
|
||||
// importantly the user's max-resolution cap from
|
||||
// setPlayingFrom/applyMaxResolutionCap, so toggling
|
||||
// audio-only off then streamed UNCAPPED resolution
|
||||
// until the next setPlayingFrom (data-saver leak).
|
||||
controller.trackSelectionParameters = controller.trackSelectionParameters.buildUpon()
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, audioOnly)
|
||||
.build()
|
||||
Toast.makeText(
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ fun SearchScreen(
|
|||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = rememberBottomContentPadding(),
|
||||
) {
|
||||
items(filteredResults) { item ->
|
||||
items(filteredResults, key = { it.url }) { item ->
|
||||
ResultRow(
|
||||
item = item,
|
||||
onClick = { onOpenVideo(item.url, item.title) },
|
||||
|
|
|
|||
|
|
@ -129,9 +129,14 @@ class IosSafeHttpDataSource(
|
|||
if (chunkRemaining > 0L) chunkRemaining -= read.toLong()
|
||||
// If chunkRemaining hits 0 here, the next read() call will roll
|
||||
// to the next chunk via the block at the top.
|
||||
} else if (chunkRemaining > 0L) {
|
||||
// Inner ran out before its advertised end. Force chunk roll on
|
||||
// next read() so we re-open at the next position.
|
||||
} else if (chunkRemaining != 0L) {
|
||||
// Inner hit EOF. Force a chunk roll on the next read() so we
|
||||
// re-open at the next position. `!= 0L` (not `> 0L`) so the
|
||||
// unknown-end case (LENGTH_UNSET inner → chunkRemaining < 0)
|
||||
// ALSO rolls: with `> 0L` a negative chunkRemaining never reset
|
||||
// to 0, so the top-of-read() roll block (gated on `== 0L`) never
|
||||
// fired and we re-read the exhausted inner forever — playback
|
||||
// truncated to the first chunk.
|
||||
chunkRemaining = 0L
|
||||
}
|
||||
return read
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue