Commit graph

78 commits

Author SHA1 Message Date
b78e7187c0 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.
2026-10-15 13:07:33 +00:00
bdf9b35ebd 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.
2026-10-15 12:37:51 +00:00
a08f3b956c 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.
2026-10-15 12:25:49 +00:00
f2d63fdb1c 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.
2026-10-15 12:25:25 +00:00
21d8290f1d 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.
2026-10-15 12:25:12 +00:00
b2af6fc1a5 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.
2026-10-15 12:25:12 +00:00
aea649eab9 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.
2026-10-15 12:25:12 +00:00
8eeffe834d 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.
2026-10-15 12:25:12 +00:00
349c1aa447 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.
2026-10-15 12:25:12 +00:00
6307addfb8 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).
2026-10-15 12:25:12 +00:00
9bdca8c985 docs: contract for the issue workflow
Agents reference issues (never close — the closing keywords auto-close
on push, which mis-links fixes to unrelated commits); Jeena closes after
his TV check. Written down so it survives past sessions.
2026-10-14 05:18:27 +00:00
d99c71a67a search: usable empty state, left-out rule, vidiverse scope
The Search screen's empty state replaced the whole grid with "Nothing
here yet" — no search field, no way back; LEFT from it went nowhere.
Both are scope rules of the issue: the screen keeps its field focused
on entry (retrying the request — the focus target attaches late in a
cold grid composition), LEFT from the field opens the sidebar like on
every other screen, and the results' auto-focus only runs when a search
is actually active (a persisted query re-entered shows its results
focused on the first card).

Search scope is a chip row below the field: Local and — when /config
allows the search index — Vidiverse. Vidiverse queries go through
search/videos with searchTarget=search-index; results may live on
other instances: their cards use the absolute thumbnailUrl, and opening
one plays it from THAT instance via an anonymous API (no history
reports for remote videos).

Closes #20
Closes #21
Closes #22
2026-10-14 01:35:31 +00:00
52a0ba8fb9 loading: shimmer placeholders on every list screen
The placeholders only existed on the home rows; the shared list screens
(My videos, Search, Subscriptions, History, channel pages) still used a
full-screen spinner, and the Channels screen did too. All of them now
show card-shaped placeholders: the in-grid initial pass keeps the page
header and a two-row placeholder block, growing lists swap the always-
visible bottom spinner chip for a placeholder row driven by the new
loadingMore state on PagedListViewModel, and the channels screen shows
stub header bars plus placeholder rows while it starts up.

Closes #19
2026-10-14 00:53:26 +00:00
4156704ffe build: sign the release variant with the debug key
On TV hardware the debug build's UI thread renders at ~2fps
(dumpsys gfxinfo: 87% janky frames, 50th percentile 85ms, stalls up
to 4200ms while the GPU sits at 12ms). An R8 minified release of the
same code turns the identical interaction into 184 frames at 27ms
median with 15% jank — the artifact is the deliverable, and the debug
signature keeps install -r seamless for sideloads.
2026-10-14 00:53:05 +00:00
05bcc55aac loading: placeholder cards with a faint shimmer; thumbnail while buffering
Rows that were still loading kept silent or a bare "Loading…" line.
Every list row (continue, new videos, subscriptions) now shows dummy
cards shaped like the real thing — gray rectangles with a very faint
sweeping sheen — because that says what is coming and that it is
moving; the spinner only survives where there is nothing to shape.

Fullscreen video loading shows the video's own thumbnail dimmed under
the circle loader instead of black, at least until the detail response
has the thumbnail; before that only the circle exists (issue #19).

The shimmer is a linear sheen at 8% alpha looping over 1.6s on a
near-black base, subtle per the report.
2026-10-14 00:26:26 +00:00
b2601bb0c0 player: debounce the buffering indicator
ExoPlayer flaps READY↔BUFFERING at every buffer edge and seek; each
flap tore the red spinner overlay out of composition and rebuilt it,
visibly restarting the animation from zero and flickering the covering
box — the "struggles and restarts" report. The displayed state now
follows the raw player state debounced, last value wins after 200 ms,
so quick flaps cancel out and the overlay never churns mid-playback.
The long initial buffering still shows it immediately.

Closes #17
2026-10-14 00:20:50 +00:00
33abf61cd1 home: rows grow on demand; refocus content when the drawer closes
The three home rows were capped: continue at six, subscriptions at
twelve, with no way to move past. Continue and subscriptions now page
in place — each new page fetches only its own entries (the continue
row's resume info costs one detail call per entry, so a page enriches
what it appends and never re-reads earlier ones; the "New videos" row
already paged). Rows stay lazy lists and the image cache is LRU, so a
user scrolling far does not pile up memory.

Closing the drawer without a selection (RIGHT, tap-outside, BACK) also
turned out to orphan the D-pad — the drawer's focused item left the
composition and nothing took its place; every later press died. The
closing now bumps a pulse the screens observe to re-focus the last
focused card.

Closes #18
2026-10-14 00:18:57 +00:00
1f8616d26f channel page: metadata strip between header and videos
The page knew little more than the display name. Between the identity
row and the video cards it now shows subscribers, the video count,
creation date and the owning user — all from the channel payload the
header already fetches, plus an owner/platform line and a short
description snippet. The per-channel detail response carries no video
count, so the paged list's first-page total feeds it instead of a
default zero.

Closes #14
2026-10-14 00:09:15 +00:00
44a39b83e9 drawer: open focused on the current destination
Opening the menu landed focus on Home regardless of where the user was;
the sensible start is the item that already carries the selected state.
From there all other destinations are a D-pad step or two away.
2026-10-14 00:02:40 +00:00
6d1cc8986a cards: privacy badge in the meta row, views as icon
The privacy tag sat next to the channel name — long names crowded it —
and its light-on-light colors became illegible the moment a card got
focus (white background). It moves to the last row (age · views) and
keeps a dark chip in both states. Views render as the channel card's
play icon + count instead of "1 view" text.

Closes #15
2026-10-14 00:02:40 +00:00
f0031d25c8 lists: don't open the drawer on UP
SmartTube keeps the focus in place when you press up on a list's first
row — the drawer popping was surprising ("why does the sidebar show up
when I go up"). The upToTopBar wiring that did it is removed everywhere;
the drawer stays reachable via LEFT at any row start, which is the other
half of the rule.
2026-10-13 23:37:29 +00:00
64a01d8ca2 lists: derive cards-per-row from screen and card width
The adaptive grid resolves to exactly four cards on a full-width
landscape TV pane — but a portrait-mounted or otherwise narrow TV gets
fewer columns and the hardcoded row start broke there. Compute the
count with the same arithmetic Compose uses (pane padding minus card
minimum, integer division) so the drawer-from-row-start rule tracks
the real grid on every mounting.
2026-10-13 23:33:06 +00:00
effc2bd9ae lists: constant cards-per-row instead of measuring the grid
The adaptive grid resolves to exactly four cards on the TV's fixed full-
width pane — index % 4 needs no layout probing and cannot be skewed by
spanned items. Measuring existed only to re-derive the constant.
2026-10-13 23:24:12 +00:00
ec2b1f03b1 lists: measure a card, not the header, for cards-per-row
LEFT from any card of a grid's first row opened the menu drawer. The
cells-per-row estimate read the width of the first visible grid item,
but near the top that is the page header, which spans the whole width —
so it computed one card per row and treated every card as a row start.
Skip full-span items when estimating; row starts resolve correctly even
while the header is on screen.
2026-10-13 23:17:03 +00:00
710a0f23cc channels: drop the all-channels listing walk
The 13-page listing only produced rows that were never rendered
(followed-instance groups had no UI consumer) and 12 of its pages were
spent finding locals already known. What the cards still show and the
video listing cannot carry — banners, exact subscriber counts — comes
from one per-channel call per local channel, in six parallel batches.
Zero-video local channels are no longer listed: they had nothing on
their cards anyway; the signed-in user's own channels stay complete
via the account listing.
2026-10-13 23:03:58 +00:00
29dee24849 channels: build the local rows from the video listing
Entering Channels walked thirteen 100-row pages of the all-channels
listing (1237 rows on this instance, most of them federated follows)
and re-sorted the rows after every page; the signed-in user's own row
only appeared at page 12, which read as "loading one channel after
another" and never kept icons visible above the fold.

The local video listing already carries what the cards show: paging its
few pages yields exact per-channel video/play counts, covers, channel
avatars and the owner accounts for the row headers — no per-channel
requests. The all-channels listing now runs behind it as a background
metadata refinement (banners, exact subscriber counts, zero-video local
channels, followed platforms) and rebuilds rows only when a group
actually changes, so the visible rows don't churn. Re-entering the
screen within five minutes repaints the kept rows untouched.

Closes #13
2026-10-13 14:25:31 +00:00
f5b37dd21d player: wire the D-pad through the action row, panels and playlist deletion
The action-row buttons lost their modifier in the icon-only rework and
became unfocusable, so D-pad DOWN off the paused seek bar landed nowhere
(requestFocus failed with "not initialized"). The modifier is attached
to the button again and the seek bar ↔ buttons directions are wired
explicitly, because the default focus search skips the bar.

Pill visuals use the new state language: no border, gray pill = focused,
red glyph = subscribed/rated; the sidebar panels keep the D-pad inside
their lists (consumed at the edges). BACK/Esc acts on ACTION_DOWN only —
its trailing UP saw the panel already closed and exited the video — and
panels return focus to the pill that opened them.

Playlist deletion is a requested scope addition: holding OK on a row
asks via a dialog (DELETE /video-playlists/{id}); the toggle moves to
key release so the hold cannot double-act. Holding also exposed an
activation bug: the CENTER helper consumed only ACTION_DOWN and the
release leaked into the underlying clickable — now consumed, repeats
swallowed, so Create (and the cards) fire exactly once per press.

An info panel shows the video description with clickable links: inline
(pointer) plus a focusable link row list (D-pad); PeerTube video and
channel links play/drill in-app, other links open the browser.

Closes #12
2026-10-13 14:25:21 +00:00
c70d98ac64 home: Drop the logo 2dp more for balance
At 16dp bottom padding the glyph floated 3-4dp under the title baseline
and read as loosely attached; at 14dp the TV's bottom edge touches the
baseline so it reads as an object standing next to the text.
2026-10-13 09:26:35 +00:00
3d4de90671 player: Align the controls overlay; consistent stat icons
The overlay's spacing came out lopsided: 34dp between the seek bar and
the action row while the title/channel line sat tight, the first pill's
glyph ~50dp from the screen edge vs the 24dp-aligned text behind it, and
the time text at a different right margin than the track it belongs to.
Positioning recipe (measured on the emulator, cross-checked with a
second model): seek-row box 28dp with no horizontal padding so the track
spans the same 24dp margins as the text, action row 6dp below it, first
pill drops its start padding so the glyph aligns flush with the text
margin, time padded 3dp so its right edge tracks the rounded cap.

The stat icons now match the Channels view (Groups = subscribers,
PlayArrow = views next to the numbers), the overlay's subscribe became
Groups instead of the YouTube-notifications bell (the bell read as
notifications), and federated channels without an avatar get a
letter-circle fallback so the channel row keeps its shape.
2026-10-13 08:29:43 +00:00
3d5fd44a59 channels: Parallelize stats and persist them
The channels screen enumerated every channel and then fetched each
channel's stats strictly one after another, with a row rebuild and a
row re-save after every single one — with several dozen local channels,
each stat call walking up to four video pages, that took minutes.
Stats now fetch in parallel batches of six and are persisted alongside
the rows snapshot, so an existing install paints the channels screen
with full stats instantly.

Closes #9
2026-10-13 08:29:43 +00:00
01c08e1dbb player: Icon-only action row
Bell, thumbs, chat bubble and playlist icons instead of text labels —
no translutable strings in the action row; labels live on as
contentDescription for accessibility. Like/dislike get proper icons
instead of emoji text with the count as a digit next to them.
2026-10-13 07:51:44 +00:00
4af904bf2a home: Nudge the logo below the text baseline
On one baseline the glyph looked like it was sinking into the title;
1dp lower matches how the launcher icon sits next to text.

Closes #11
2026-10-13 07:45:17 +00:00
9ca65c121b player: Action row with subscribe, rating, comments and playlists
SmartTube keeps its action buttons under the seek bar — we had nothing.
The paused/controls overlay gained: subscribe (POST /users/me/
subscriptions with the channel URI, state via the bulk exist endpoint),
like/dislike with immediate count and fill feedback (PUT /videos/{uuid}/
rate), and right-sidebar panels for comments (GET /videos/{uuid}/
comment-threads, paged, read-only; the instance has comments disabled on
some videos, which surfaces as a friendly empty state) and playlists
(checkboxes add/remove the video via the playlist-elements endpoints, a
text field creates new ones). Opening a panel shrinks the video so it
stays fully visible on the left; BACK or LEFT closes again.

Two server quirks are handled: the subscription exist-check settles a
moment after the POST (optimistic update + delayed re-verify instead of
trusting an instant answer), and this emulator translates CENTER into
the platform click as well, so a single press could subscribe and
instantly unsubscribe — activation is now guarded per request.

Closes #10
2026-10-13 07:45:17 +00:00
9f1c6d54c1 cache: Show the last session's lists instantly at cold start
Opening the app (especially the Channels screen) waited for the API and
then for every image before anything painted. JSON list snapshots now
live in cacheDir: on the next start the last loaded page paints
immediately, then the load that runs anyway silently replaces the first
page with fresh data, so stale content can never stick (issue asks
exactly for that show-cached-revalidate behavior). Logout wipes the
snapshots because they contain account-private listings; History and
Search results are never snapshotted. Thumbnails were already
disk-cached by Coil (the instance serves them with a one-year max-age).

Closes #9
2026-10-13 07:45:17 +00:00
fdc570ac9e home: Title bar with account and channel pages
The header-removal experiment went too far: the app now has a slim
persistent title bar — logo + "JTube TV" on the left (it anchors the
layout while the drawer is open), avatar + display name on the right as
a purely visual indicator so it never steals D-pad focus. LEFT from the
first card of every row opens the drawer (the subscriptions row silently
skipped it, and the grid only wired the very first item), and UP no
longer surprises by opening anything.

The channel page that the Channels screen and the player's channel icon
point at never actually rendered — ListSource.Channel, its repository
call and the header state all existed without a UI. Videos of one
channel now open as a grid with banner, avatar, name and handle; page
headers (search field, channel banner) live inside the grid so they
scroll along instead of stacking under the cards.

Closes #7
2026-10-13 00:05:30 +00:00
a6dff3435d player: Own the controls UI end to end
Space and Enter now toggle playback while playing (the remote OK always
did), and the native PlayerView controller is switched off completely:
hovering the video with a mouse revealed the old play UI with skip
buttons and a settings gear, which must never appear. Our SmartTube-style
overlay is the only controls surface — persistent when paused, shown
transiently for 5 s while playing — with the transport icon row dropped,
position/duration in one style right of the bar, the absolute published
date under the title and a clickable channel icon that opens the
channel's video page.

The loading indicator now tracks the player's real buffering state
(STATE_READY) instead of ending with metadata resolution, so the window
is never black without an indicator. Closes #8
2026-10-13 00:05:19 +00:00
15a481b7c0 profile: Fetch the user profile on every app start
The login flow stored displayName/avatar inside one swallow-all try that
serverConfig() also had to survive first, and /users/me carries both in
its nested account object on current PeerTube versions (verified live
against 8.3.1) — so stored sessions kept showing the raw username and no
avatar. The profile now refreshes after login and whenever an existing
session starts, and the relative avatar fallback no longer points into
/api/v1.
2026-10-13 00:05:10 +00:00
a71de07969 emu: Stop silent early exits in the emulator start script
The script's set -euo pipefail made it die without any visible error in
two places: an empty /tmp/jtube-adbkeys/ failed the glob cat (the error
itself was discarded, so the output just stopped mid-script — this hits
any host after a reboot), and the boot-wait loop was killed on its first
iteration because adb exits non-zero while the device is still booting.
Both were swallowing the whole && chain (adb connect, scrcpy), making it
look like the emulator was broken when it was fine or about to be.

The stale bridge-emulator.sh duplicate (different port, broken vendor
keys path) is removed in the same spirit — keeping it invites running
the wrong script. Docs updated to match the one script.
2026-10-13 08:14:15 +09:00
c19024355c player: Usable D-pad controls when paused
The paused state was unusable (#4): the semi-transparent info overlay
covered the seek line, and the key handler kept owning CENTER/LEFT/RIGHT
(they toggled/seeked directly), so the transport buttons could never be
selected at all.

Replace it with a SmartTube-style pause screen: a big seek bar above an
info bar (never covered again — the panel below it is near-opaque) and
focusable Back 10 s / Play / Forward 10 s buttons. While paused the
arrows pick a control and OK activates it; the bar itself scrubs ±10 s
with arrows and OK starts playback. Focus moves into the overlay on
pause (Play button) and back to the plain key handler on resume, so
while playing, OK still toggles pause and LEFT/RIGHT still seek.

Refs #4
2026-10-12 09:54:58 +00:00
1c8fff7eca home: Make the "New local videos" row reachable by DOWN
On the TV, DOWN from "Continue watching" often skipped the row below
and landed on "New from your subscriptions": the new-videos row is
rendered as a non-focusable "Loading…" while its items are still in
flight, and every FocusScrollRow auto-focused its first card when its
items arrived, so rows loaded out of order stole focus in a race.

Start fetching new local videos eagerly with the rest of the home data,
restrict the load-time auto-focus to the first row only, and cancel it
when the user has already pressed a D-pad key meanwhile. Also rename
the row to "New local videos" to distinguish it from the federated
subscription feed.

Refs #3
2026-10-12 09:49:15 +00:00
20e6b07107 subs: Show the subscription feed as one paged video grid
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
2026-10-12 09:47:18 +00:00
3635a4ed74 home: Restore row focus when returning from the player
BACK from a video dropped the user on a random spot (often the search
screen, #2): the restore token was only bumped when it was already >= 1,
so the very first back never re-focused anything, and HomeRowsViewModel
refresh() cleared the continue-watching rows synchronously, disposing
the rows before their focus-restore effect could run. Bump the token on
every playing -> not-playing transition and keep the rows alive during
a refresh instead.

Refs #2
2026-10-12 09:42:09 +00:00
40a9d1e2c6 docs: Agent container emulator workflow
Document how the host emulator is driven over adb from the agent
container: the start script now boots headless by default and trusts
remote adb keys via ADB_VENDOR_KEYS at launch, since the TV system
image is a user build that blocks pushing keys to adb_keys after boot.
AGENTS.md points to the new doc and fixes the stale SDK path.
2026-10-12 09:42:08 +00:00
ed65b77e90 player: Pause and mute out when the app goes to the background
Pressing HOME kept the audio running, so a video started in another
app played on top of it and returning landed on the home view with an
invisible player. The player now pauses itself on ON_PAUSE, reports
the position to the server and resumes automatically when the app
comes back. The playing screen is also kept across activity
recreation now, so returning shows the video, not the home view.

Ref #6
2026-10-12 03:23:34 +00:00
a5d88aeea1 docs: Real screenshots
Replace the placeholder-content screenshots with a demo instance that
serves real CC-licensed open movies (Blender Foundation, framatube),
so the app looks like what it is: a video player with real thumbnails,
real durations and real playback.
2026-10-12 03:06:15 +00:00
344a211d72 player: Fix vertical videos stretched to full screen
On the HiMedia/Amlogic TV the video renders through the vendor video
plane, which scales the decoded stream to the whole panel and ignores
the letterbox geometry AspectRatioFrameLayout lays out for the
SurfaceView. Portrait videos therefore filled the screen instead of
being letterboxed; on emulators the same code was correct.

Render the video in a TextureView instead: it lives in the normal
view tree, so the aspect ratio is honored on every device. Verified
on the HiMedia box (vertical short letterboxed, landscape fills the
screen as before) and on the emulator.

Closes #1
2026-10-12 02:48:11 +00:00
116b6926e4 docs: README for humans, with logo and screenshots
Rewrite the README around what a user sees: what the app is for, how
the screens look and what to press. Add the logo, five screenshots
taken against a local mock PeerTube instance with placeholder content
(no private media), and a debug-only manifest that allows plain HTTP
so those mock instances work.
2026-10-12 01:18:02 +00:00
669c4af359 license: Adopt GPL-3.0-or-later
Decide on the license before the source is shared publicly: GPL v3
keeps forks and improvements of this app free software too.
2026-10-12 00:41:17 +00:00
0cebe3b710 docs: Remove the test account name from the public repo
The account name identifies a real, seeded account on the instance;
the public source doesn't need it to make sense. Credentials were
never in the repo and still live only in sops.
2026-10-12 00:33:42 +00:00
89c1a13b8b docs: Drop instance-specific storage-security notes
The public repo must not document this instance's object-storage
access-control posture; findings like that belong in private ops
notes. The engineering guidance (never rely on URL obscurity, always
request file tokens) stays.
2026-10-12 00:27:08 +00:00