Compare commits

..

29 commits

Author SHA1 Message Date
d016805688 channels: sort rows on stable keys only — subscriber counts shifted
everything mid-load

Making subscribers the primary key reordered both rows and whole rows
as the per-channel subscriber refine streamed in: each background batch
re-sorted the screen while loading, and the list jumped several times on
entry (user: "the jumping is terrible"). The subscriber counts are not
in the video-listing fast path; they only arrive seconds later in
per-channel batches, so any ordering that leads with them cannot render
early.

Back to the stable keys — own row first, then video totals (then plays,
then subscribers) on both the row and the channel level. These come
entirely from the video listing before the first rebuild, so the screen
painted correctly once, verified: a single rebuild line across the whole
TV load, no further reshuffling. Video-less channels still never render,
the own row still holds first place whenever it exists.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-19 14:40:26 +00:00
b73958d617 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.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-19 14:28:11 +00:00
31846f6eaf channels: keep the logged-in user's row when it has no videos
The per-user rows are built from the local video listing, so a channel
that never uploaded is invisible to the row build — and an account that
owns only such channels lost its row entirely. tvbot is exactly that
case: the local tvbots channel exists, it has no videos, and while
logged in as tvbot the screen opened on the next user's channels.

The row build now merges the logged-in user's own local channels from
the account-channels listing (already fetched, carries the full card
data) into the locals before grouping; the group sort already puts the
own row first. Verified on the emulator logged in as tvbot: the
"tvbot's Channels" row renders first with the video-less tvbots card.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-19 14:09:27 +00:00
b55288b02d emu: prefer the working host SDK over the broken home install
The emulator in ~/Android/Sdk auto-updated to 37.2.12 on Oct 11 and since
then consistently fails its own KVM probe ("/dev/kvm is not found") and
segfaults on that failure path — while KVM is healthy: a manual
KVM_GET_API_VERSION ioctl returns the current API level 12, and both
/opt/android-sdk and the container-home emulator pass -accel-check on
the same kernel and device node. The script now prefers the documented
host SDK at /opt/android-sdk when no ANDROID_SDK_ROOT is set.

Once the home SDK is repaired (or the emulator regression disappears),
the fallback order can be revisited.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-19 13:38:52 +00:00
730bda0b40 emu: pin windowed mode to the integrated GPU
The discrete card now runs continuous AI inference, so the emulator must
not claim GL/Vulkan contexts on it. In windowed mode the emulator uses
the host GL/Vulkan stack, and the default GLVND selection picks the
discrete card on this machine. The script now discovers the first
display device whose driver is not nvidia and pins the emulator process
to it (Mesa GLX/EGL + DRI_PRIME + the Intel Vulkan ICD).

Headless (the default) needs no change: it always ran on swiftshader,
strictly software, and never touched any GPU.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-19 13:30:52 +00:00
e9cb32b173 ui: cap the eased scroll and drop the snapshot focus map
A system trace on the TV showed the eased focus scroll re-ticking for
hundreds of milliseconds per move — animateScrollToItem's default spec
scales with distance, and every tick redraws the whole window on a TV
GPU, with 145 ms spikes when new cards composed mid-animation. That is
what drags row scrolling back down to a few fps, again (same symptom
51077b8 and 5ea1ca5 each fixed a facet of). easedScrollToItem keeps the
eased feel but caps the tween at ~140 ms, so a move costs a handful of
cheap ticks instead of hundreds.

The per-card focus requesters in the video grid were back in a snapshot
state map written during composition — the exact recompose loop
5ea1ca5 fixed in the rows, still alive in the grid screens. Plain
HashMap again, keyed on epoch only: keyed on items.size the map was
dropped wholesale on every page load, and already-composed cards didn't
re-populate it until their next recompose.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-19 13:08:18 +00:00
96c53c6c89 images: rearm the loading shimmer only when the image changes
ShimmerAsyncImage keyed its loaded-state on the passed model, and the
callers built a fresh coil ImageRequest on every recomposition. ImageRequest
has no value equality, so the key changed every time: the loaded flag
reset, the shimmer overlay was re-added and the image load restarted with
each recomposition — an infinite animation per card that never disposed.
On the TV this showed as a general fps collapse wherever thumbnails
render (issue #28's feature shipped with this trap).

The requests are now remembered under stable keys (video/channel id and
the URL strings), so the sweep stops for real once the image (or an
error) lands.

Addresses #28.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-19 13:08:09 +00:00
afb3a0c871 channel page: park the grid unconditionally, tighter header band
The old park ran only while `firstVisibleItemIndex > 0`, but the focus
pull that hands the first card its focus leaves the header row at index
0 while slid up by ~96 dp — so the guard never fired and the top of the
channel header stayed clipped off screen (#31 review). scrollToItem(0)
re-anchors both index and offset.

The banner band drops to 116 dp with a 40 dp description strip, so the
whole header is under one card row: the grid never reflows around it and
the header stays fully on screen when the first card takes focus.

Addresses #27, #31.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-19 13:08:00 +00:00
ab1890b931 player: scroll the info text in thirds of a screen (#23)
One D-pad press moved 85% of the panel, so consecutive stops barely overlapped
and the middle of a long description was never on screen — on
tube.jeena.net/w/p4HtZrDCMbPfGYpMfG9fMN Jeena could not read the passage about
the credit-card outlier at all (#23 review). A press now moves about a third of
a screenful, so each stop repeats two thirds of the previous one and every line
gets shown on the way down.
2026-10-16 13:04:37 +00:00
d338e50375 channels: never open the screen without a selection (#32)
Coming back from a channel page hands that channel's card the focus, but the
handle only wins when the card is still in the rows. A channel that was
renamed, unsubscribed, or simply never carried in the rows left every row
asking for no focus at all, so opening Channels from the drawer put the remote
nowhere: "when I click on Channels, none is selected, it should behave like
video cards" (#32 review). The fallback row is now chosen by whether the
handle exists anywhere in the rows, and a row whose wanted card never became
focusable takes its first card instead of staying unselected.
2026-10-16 13:03:28 +00:00
06a0fabf33 channel page: park the grid before the first card takes focus (#27, #31)
Two things were still wrong on the channel page and the list screens.

The header is a grid item again, so it scrolls with the videos (#27 review),
but handing the first video card the focus makes the framework pull that card
fully into view: the grid animates up, and the old park-at-top effect then
snapped it back down. Jeena still saw that jump "when the first video gets
focus". The park now runs before the focus handover, and only while the grid
is actually displaced, so there is nothing left to undo.

The first loading card on My videos, Search, Subscriptions, History and the
channel pages used to span the whole line, which read as a wide band across
four columns; Jeena wanted the plain single card in one column of one row
(#31 review), so the placeholder item no longer spans the line.
2026-10-16 13:03:04 +00:00
ddacf48906 player: match the normalised D-pad key when scrolling the description (#23)
Scrolling the info text up still did nothing on the TV while down worked. The
handler only matched the raw nativeKeyEvent keycode; remotes don't all report
the same codes, so match Compose's normalised Key.DirectionUp/Down as well, and
always attempt the scroll (animateScrollBy clamps at the ends itself) instead of
gating it on the current offset.

Not reproducible on the emulator in either the debug or the release build, so
this targets the device difference rather than a confirmed fault.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 14:56:00 +00:00
654963436f channels: load at launch, keep the opened channel selected (#32, #34)
Assembling the channel rows is slow, so it now starts when the app launches
instead of when the view is first opened; opening the view usually finds the
rows already there, and a second pass is skipped while one is in flight or the
data is still fresh (#32).

Coming back from a channel page re-selects the channel that was opened, scrolling
its row back to it, instead of snapping to the first one (#34).

Also drops the custom focusRestorer fallback added for #29: handing back a
requester whose card had scrolled out of composition crashed the app with
"FocusRequester is not initialized" when focus re-entered a scrolled row (hit on
the Channels screen). The plain restorer keeps the #29 behaviour without it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 14:21:10 +00:00
105e1c82fe channel: header scrolls with the videos again, reserved height, fallbacks (#27)
Review follow-ups on the channel page header:

- It is a grid item again instead of being pinned, so scrolling the videos moves
  it out of the way rather than leaving them to slide behind it. The redesigned
  header is compact enough to stay fully visible when the first card takes
  focus, and the page parks itself at the top on entry so the Subscribe button
  is on screen.
- Its height is reserved before the channel loads, so the cards no longer jump
  down when it arrives.
- A channel with no banner gets a gradient derived from a hash of its handle
  instead of flat black, and one with no icon gets a stand-in avatar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 14:21:00 +00:00
f8a36a73e5 player: sidebar starts at the top, links stay in the text (#23, #24)
The info and comments panels sat under a hard-coded 48dp top padding, leaving a
black strip above them; they now fill the height from the very top (#24, and the
same note on #23).

The info panel no longer repeats the description's links in a list underneath —
they stay inline in the running text and OK opens the first one. UP/DOWN are
always consumed while scrolling, so the panel can no longer strand focus in a
place it can't scroll back from (#23 review).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 14:20:52 +00:00
904020da3c player: no spinner over the last frame when a video ends (#33)
STATE_ENDED counted as "not ready", so finishing a video brought the thumbnail
and the spinner back over the picture. Only STATE_BUFFERING drives the overlay
now; the initial load is already covered by the first-frame guard, so the video
simply stops on its last frame with the paused controls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 14:20:44 +00:00
89171b0b0d loading: one loading card on the grids, not a full fake grid (#31)
My videos, Subscriptions, History and the channel pages painted two whole rows
of placeholder cards on first open, and Channels painted two stub rows. Show a
single loading card instead, matching the Home rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 14:20:35 +00:00
c3b5908f31 player: keep the thumbnail until the first frame, not just STATE_READY
ExoPlayer reports STATE_READY a beat before it actually draws the first frame,
so the buffering thumbnail was removed into a black gap right before playback
("thumbnail, then black, then playing"). Hold the thumbnail overlay until
onRenderedFirstFrame fires, so it hands straight over to the picture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 13:46:26 +00:00
fcb3be8e96 player: show the card thumbnail as a poster while the video loads
Opening a video flashed plain black during the metadata-loading phase (before
buffering, where the thumbnail already shows) — most visible on the TV where
that phase is longer. Pass the opened card's thumbnail into the player and paint
it full-screen (dimmed, under the spinner) during loading, handing off to the
buffering thumbnail once the stream is ready, so the player opens onto the
video's own poster instead of black.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 13:33:12 +00:00
46a325eed3 docs: regenerate screenshots via the mock backend, add the channel page (#30)
Recapture every screen through the branch-only mock backend
(tools/screenshot-mock) so the app runs unmodified against real CC/Blender
content: the rows no longer repeat thumbnails (Continue watching, New local
videos and My videos draw different slices), the account has an avatar, the
player sits on a frame with something happening, and the Subscribe button reads
correctly. Adds a channel-page screenshot (banner, stats, Subscribe, full video
grid) and references it in the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 13:07:33 +00:00
e9d7b473fb docs: refresh the README screenshots (#30)
New screenshots of the current UI (title bar with the account on the right, the
redesigned Home with three rows and progress bars, My videos privacy badges, the
Channels rows and the player). Captured against the public PeerTube demo instance
(peertube2.cpy.re) so they show real CC/Blender content, as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 12:37:51 +00:00
e8117ab08e docs: rewrite the README for humans, add the PeerTube logo (#30)
Rewrite the README so it leads with what JTube TV is and who it's for, refresh
the feature list and screenshot captions to match the current UI (three Home
rows, Back opens the sidebar, Subscribe on the channel page), and add the
official PeerTube logo next to the name. Screenshot images are refreshed in a
follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 12:25:49 +00:00
069dfe38c4 docs: remove BACKLOG.md
Future work is tracked as Forgejo issues now; the standalone backlog file was
redundant. AGENTS.md and README.md no longer reference it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 12:25:25 +00:00
39d3b12182 channel: redesign the header, reachable Subscribe button (#27)
Without a mouse the channel banner was invisible: opening a channel focused the
first video card, which scrolled the header (a grid item) off-screen. The header
is now pinned above the grid and never scrolls away. The banner is its backdrop,
darkened to black at the bottom; the avatar sits beside the title with a
"by <owner> on <platform>" byline, the subscriber/video/created stats, and the
description flowing into the black beneath (a long one grows the header down
instead of being clipped). A Subscribe button sits on the banner, reachable with
UP from the first card, wired to the same follow/unfollow as the player.

AGENTS.md: subscribing is in scope (already on the player, now the channel page
too), so drop it from the excluded list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 12:25:12 +00:00
84a0527e20 images: shimmer placeholder while thumbnails and art load (#28)
Thumbnails, channel banners and avatars popped in from a flat grey rectangle. A
new ShimmerAsyncImage shows the same faint shimmer as the card placeholders
while the image loads, and the sweep disposes once the image (or an error)
arrives. Used for video thumbnails and the channel banners/avatars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 12:25:12 +00:00
4176edfd69 loading: drop the snapshot cache, one loading card per thing (#25)
The saved-list snapshot (issue #9) painted the last session's rows instantly,
then the network refresh swapped them in and the tiles jumped as newer videos
appeared — jarring. Drop the snapshot instant-paint on Home, the video lists and
the Channels screen; images stay fast because Coil already disk-caches them.

Screens now show the loading card while the first page loads instead of stale
content: one card per Home row (not a fake full row), a single card at the end
of a growing grid, and a trailing card when a Home row loads its next page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 12:25:12 +00:00
e485caa855 rows: restore the last-focused card when moving between rows (#29)
Moving up/down between the Home rows (and the Channels rows) jumped to the
geometrically-nearest card, so the row shifted left/right and felt inconsistent.
A focusRestorer on each row now lands focus on the card that row last had, so
the rows stay put as you move between them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 12:25:12 +00:00
a6d0f9ec6c home: Back opens the sidebar from a list view (#26)
Reaching the sidebar used to mean scrolling all the way left to the first card.
Back from any list view now opens it directly; from the open sidebar on Home a
double Back still exits, and Esc (keyboard/scrcpy) mirrors the same model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 12:25:12 +00:00
d2b40d5f10 player: title header + scrollable description in info/comments (#23, #24)
The info and comments side panels showed a generic "Info"/"Comments" label,
which read as an empty black bar with no clue which video it belonged to. Both
now show the video's title instead (#24, and the same complaint in #23).

The info panel's description also could not be scrolled with the D-pad: a plain
Text in a LazyColumn takes no focus, so a long description was unreadable past
the first screenful unless it happened to contain links. It is now a focusable,
D-pad-scrollable box; Down at the bottom hands focus to the link list, Left
closes the panel (#23).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 12:25:12 +00:00

Diff content is not available