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.
This commit is contained in:
Jeena 2026-10-12 01:18:02 +00:00
parent 669c4af359
commit 116b6926e4
9 changed files with 210 additions and 46 deletions

125
README.md
View file

@ -1,70 +1,103 @@
# JTube TV # JTube TV
An Android TV client for [PeerTube](https://joinpeertube.org/) instances, inspired by <p align="center">
how SmartTube works for YouTube. It talks to **any PeerTube instance** <img src="docs/jtube-logo.svg" width="150" alt="The JTube TV logo, a little orange TV" />
(the default is [JTube](https://tube.jeena.net) — my own instance) over the </p>
[PeerTube REST API](https://docs.joinpeertube.org/api-rest-reference.html).
## Features (v0.1) JTube TV is an Android TV app for [PeerTube](https://joinpeertube.org/). It puts
a PeerTube instance on the big screen the way SmartTube puts YouTube there:
browse from the couch, play with the remote, nothing else in the way.
- **Setup flow** — enter instance URL, username and password once; the app exchanges It talks to any PeerTube instance over the
them for OAuth tokens via `POST /api/v1/users/token` and stores [REST API](https://docs.joinpeertube.org/api-rest-reference.html). The default
**only the tokens** (never the password), encrypted with the Android Keystore is [tube.jeena.net](https://tube.jeena.net), my own instance, where most videos
(EncryptedSharedPreferences). are private family uploads. Signing in matters because "My videos" shows them
- **Local videos** — list public videos of the instance (`GET /api/v1/videos?isLocal=true`). all, privacy levels included.
- **My videos** — all of the logged-in user's own videos, every privacy level included
(private, internal, unlisted — `GET /api/v1/users/me/videos`). Non-public entries
carry a privacy badge.
- **Channels** — list channels local to the instance
(`GET /api/v1/video-channels?search=…&searchTarget=local`) and browse each channel's videos.
- **Search** — search local videos by title.
- **Subscriptions** — the videos in the user's subscription feed
(`GET /api/v1/users/me/subscriptions/videos`).
- **History** — the user's watch history as tracked by the server
(`GET /api/v1/users/me/history/videos`).
- **Playback** — HLS (or progressive MP4 fallback) playback with ExoPlayer/Media3.
Viewing is reported to the server (`POST /api/v1/videos/{shortUuid}/watching`) so it
shows up in server-side history. Private/internal media is streamed with the OAuth
token attached to every playlist/segment request.
Out of scope for now (see `BACKLOG.md`): uploading, editing, subscribing/unsubscribing, ## What it looks like
deleting, creating anything other than history entries.
Screenshots were taken against a demo instance with placeholder content, on an
emulator. The app itself is on a dark theme, made for a 10-foot couch view.
The home screen. Continue watching picks up at the position the server saved,
new videos first, page after page.
<p align="center">
<img src="docs/screenshots/home.png" width="800" alt="Home screen with Continue watching and New videos rows" />
</p>
A slide-in drawer opens with the left D-pad button. My videos shows your own
uploads, every privacy level included, with a badge on anything not public.
<p align="center">
<img src="docs/screenshots/drawer.png" width="800" alt="Menu drawer with the destinations" />
</p>
<p align="center">
<img src="docs/screenshots/myvideos.png" width="800" alt="My videos grid with Private and Internal badges" />
</p>
Channels, one row per owner, with video and play counts on every card.
<p align="center">
<img src="docs/screenshots/channels.png" width="800" alt="Channels screen with channel cards" />
</p>
The player speaks remote: play and pause, seek back and forward, a seek bar
with position and duration. Watching is reported to the server every 20
seconds, so where you left off survives the app.
<p align="center">
<img src="docs/screenshots/player.png" width="800" alt="Player with controls, seek bar and title" />
</p>
## What it does in v0.1
- Sign in once. The app exchanges your username and password for OAuth tokens
and stores only those, encrypted with the Android Keystore. The password
itself is never kept.
- Home with Continue watching and New videos.
- Browse the instance's public videos, and search them by title.
- My videos: every video you own, private, internal and unlisted included.
- Channels: local channels with their videos, plus your subscriptions and your
server-side watch history.
- Player: HLS, with a progressive MP4 fallback, resume from the saved
position, and watching reports.
What it deliberately does not do (yet, or ever): uploading, editing,
deleting, managing subscriptions. That's tracked in
[BACKLOG.md](BACKLOG.md).
## Build ## Build
Requirements: JDK 17+ (JDK 21 recommended), Android SDK platform 35. Requirements: JDK 17 or newer (21 recommended) and an Android SDK with
`local.properties` with `sdk.dir=…` is picked up automatically if present platform 35. The Gradle wrapper is checked in, so no local Gradle install is
(standard Android Gradle Plugin behavior). needed.
```bash ```bash
./gradlew :app:assembleDebug ./gradlew :app:assembleDebug
``` ```
Install on a TV device: Then put it on the TV:
```bash ```bash
adb connect <tv-ip>:<port> adb connect <tv-ip>:<port>
adb install -r app/build/outputs/apk/debug/app-debug.apk adb install -r app/build/outputs/apk/debug/app-debug.apk
``` ```
> Building requires a JDK; the Gradle wrapper is checked in, so no local Gradle ## Architecture
> installation is needed.
## Architecture overview A single-module app, deliberately small:
Single-module Android TV app (`app/`): - `data/api` — Retrofit interface and kotlinx-serialization DTOs for the
PeerTube API.
- `data/api` — Retrofit interface + kotlinx-serialization DTOs for the PeerTube API. - `data/auth` — TokenStore, the encrypted preference storage for tokens.
- `data/auth` — `TokenStore` (EncryptedSharedPreferences), token acquisition/refresh. - `data/network` — OkHttp/Retrofit wiring: dynamic base URL, bearer token
- `data/network` — OkHttp/Retrofit wiring: dynamic base-URL interceptor, bearer-token interceptor, single-flight token refresh.
interceptor, single-flight token refresh authenticator. - `data/PeerTubeRepository.kt` — one suspend-call surface for the whole UI.
- `data/PeerTubeRepository.kt` — one suspend-call surface used by all view models. - `ui/` — Compose for TV screens, one for each of the destinations above.
- `ui/` — Compose for TV screens: Setup, Home (sidebar + list content), - No DI framework, no database. The service locator is `AppGraph`.
VideoList (local / my videos / subscriptions / history / search / channel),
Channels, Player. `MainActivity` is the only activity.
- `JTubeApp.kt` — tiny service locator (`AppGraph`); no DI framework on purpose.
## License ## License
JTube TV is free software: [GPL-3.0-or-later](LICENSE) — see the JTube TV is free software under [GPL-3.0-or-later](LICENSE).
[fsf.org license page](https://www.gnu.org/licenses/) for details.

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Debug builds only: allow plain HTTP so local mock instances (e.g. the
screenshot/demo server on 10.0.2.2) work without TLS. Release keeps the
default policy. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:usesCleartextTraffic="true" />
</manifest>

124
docs/jtube-logo.svg Normal file
View file

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="88.489128mm"
height="91.858803mm"
viewBox="0 0 88.489128 91.858803"
version="1.1"
id="svg5"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
sodipodi:docname="jtube-logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.6"
inkscape:cx="2e+02"
inkscape:cy="4e+02"
inkscape:window-width="1280"
inkscape:window-height="1371"
inkscape:window-x="20"
inkscape:window-y="20"
inkscape:window-maximized="0"
inkscape:current-layer="layer1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<defs
id="defs2">
<inkscape:path-effect
effect="envelope"
id="path-effect20220"
is_visible="true"
lpeversion="1"
yy="true"
xx="true"
bendpath1="M 34.505313,149.46573 114.41249,136.15862"
bendpath2="M 129.05401,151.79411 118.5642,232.91649"
bendpath3="M 40.406136,232.91649 H 118.5642"
bendpath4="m 30.189016,157.70139 10.21712,75.2151"
bendpath1-nodetypes="cc"
bendpath2-nodetypes="cc"
bendpath4-nodetypes="cc" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-1.6367512,-0.9271243)">
<g
id="g20208"
transform="translate(-33.623555,-139.58342)"
inkscape:path-effect="#path-effect20220">
<path
id="path9282"
style="fill:#f2690d;stroke-width:0.564999;fill-opacity:1"
d="m 95.530087,173.63424 c 0.237703,5.13098 -7.271828,9.10052 -16.07393,9.6703 -8.856369,0.57328 -16.58146,-2.4078 -16.889605,-7.22143 -0.150159,-2.34567 1.46186,-4.92212 4.407876,-7.07577 2.945402,-2.1532 6.967458,-3.69133 11.270092,-4.11376 4.324234,-0.42454 8.576602,0.31565 11.903131,2.04681 3.309684,1.72239 5.267101,4.20426 5.382436,6.69385 z"
inkscape:original-d="m 94.313114,181.50797 a 15.003773,8.2413044 0 0 1 -15.003773,8.2413 15.003773,8.2413044 0 0 1 -15.003772,-8.2413 15.003773,8.2413044 0 0 1 15.003772,-8.24131 15.003773,8.2413044 0 0 1 15.003773,8.24131 z" />
<path
id="rect3939"
style="fill:#f2690d;fill-rule:evenodd;stroke-width:0.287923;fill-opacity:1"
d="m 52.781211,175.80255 c 15.352225,-3.04438 37.131976,-5.29589 52.867549,-3.65988 5.11938,0.53225 9.97263,3.47783 13.41207,7.61017 3.4501,4.14516 5.162,9.07597 4.57499,13.52997 -1.02114,7.74812 -2.18435,15.07621 -3.1707,22.76067 -0.5566,4.33632 -2.85552,8.10412 -5.9399,10.97475 -3.08355,2.86987 -7.16876,5.01797 -11.41302,5.10551 -15.76926,0.32523 -31.534899,0.3254 -47.313662,0.0128 -4.218807,-0.0836 -8.076231,-2.04811 -11.016324,-4.60092 -2.936492,-2.54969 -5.323061,-5.99334 -5.948084,-10.01855 -1.111175,-7.15607 -2.461729,-14.11976 -3.466929,-21.31474 -0.577384,-4.13277 1.219869,-8.6697 4.529442,-12.55076 3.290272,-3.85843 7.910947,-6.86272 12.884568,-7.849 z"
inkscape:original-d="m 55.831553,179.70801 h 47.307227 c 8.54568,0 15.42542,6.87973 15.42542,15.42542 v 22.35764 c 0,8.54568 -6.87974,15.42542 -15.42542,15.42542 H 55.831553 c -8.545681,0 -15.425417,-6.87974 -15.425417,-15.42542 v -22.35764 c 0,-8.54569 6.879736,-15.42542 15.425417,-15.42542 z" />
<path
id="rect69-3"
style="fill:#ffffff;fill-rule:evenodd;stroke-width:0.239513"
d="m 50.288239,182.89345 c 14.410304,-2.93786 34.464441,-4.75089 48.097298,-4.20997 5.104323,0.20252 9.368283,4.85863 9.161753,9.76872 -0.28348,6.73977 -1.04037,18.83707 -1.47697,25.87606 -0.30856,4.97478 -4.32137,9.33624 -8.83652,9.40339 -14.531785,0.21609 -28.954551,-0.025 -43.845067,0.66709 -4.531823,0.21062 -8.726834,-3.23791 -9.226444,-7.59555 C 43.25331,208.875 42.045245,201.264 41.16817,193.43166 c -0.497354,-4.4414 3.743142,-9.442 9.120069,-10.53821 z"
inkscape:original-d="m 53.339744,185.91118 h 43.364702 c 4.526794,0 8.171104,3.64431 8.171104,8.1711 v 23.83674 c 0,4.52679 -3.64431,8.17111 -8.171104,8.17111 H 53.339744 c -4.526792,0 -8.171104,-3.64432 -8.171104,-8.17111 v -23.83674 c 0,-4.52679 3.644312,-8.1711 8.171104,-8.1711 z" />
<path
id="rect69"
style="fill:#21747e;fill-rule:evenodd;stroke-width:0.228989;fill-opacity:1"
d="m 49.963595,185.00085 c 13.988112,-2.83898 33.243414,-4.51581 46.278337,-4.20748 4.792958,0.11337 8.761148,4.3804 8.594248,9.05189 -0.23575,6.59906 -0.82202,17.82546 -1.20545,24.80507 -0.26396,4.80503 -4.052657,9.03225 -8.352273,9.11489 -14.004298,0.26916 -27.932009,-0.0544 -42.299092,0.65309 -4.321142,0.21279 -8.302297,-3.05797 -8.771945,-7.21845 -0.852622,-7.55313 -1.998872,-14.80994 -2.864254,-22.23679 -0.490615,-4.21054 3.498996,-8.9228 8.620429,-9.96222 z"
inkscape:original-d="m 52.942466,187.86465 h 41.816355 c 4.306699,0 7.773829,3.46713 7.773829,7.77383 v 22.67781 c 0,4.30669 -3.46713,7.77382 -7.773829,7.77382 H 52.942466 c -4.3067,0 -7.773826,-3.46713 -7.773826,-7.77382 v -22.67781 c 0,-4.3067 3.467126,-7.77383 7.773826,-7.77383 z" />
<path
id="path9120"
style="fill:#ffffff;stroke-width:1.08238"
d="m 118.64237,195.14414 c -0.13825,1.26531 -1.43696,1.9756 -2.85713,1.65537 -1.42173,-0.32057 -2.48529,-1.57183 -2.38214,-2.86501 0.10298,-1.2911 1.34481,-2.11758 2.81602,-1.7732 1.4728,0.34475 2.56174,1.71529 2.42325,2.98284 z"
inkscape:original-d="m 114.74253,198.28023 a 2.2541561,2.202522 0 0 1 -2.25416,2.20252 2.2541561,2.202522 0 0 1 -2.25415,-2.20252 2.2541561,2.202522 0 0 1 2.25415,-2.20252 2.2541561,2.202522 0 0 1 2.25416,2.20252 z" />
<path
id="path9120-6"
style="fill:#ffffff;stroke-width:1.08238"
d="m 115.97127,213.11959 c -0.13246,1.27833 -1.30912,2.1438 -2.60625,1.95391 -1.29815,-0.19005 -2.26866,-1.38507 -2.16147,-2.71244 0.10703,-1.32539 1.25819,-2.23968 2.59403,-2.01332 1.33691,0.22653 2.30637,1.49134 2.17369,2.77185 z"
inkscape:original-d="m 114.1472,215.71645 a 2.2541561,2.202522 0 0 1 -2.25416,2.20252 2.2541561,2.202522 0 0 1 -2.25415,-2.20252 2.2541561,2.202522 0 0 1 2.25415,-2.20253 2.2541561,2.202522 0 0 1 2.25416,2.20253 z" />
<path
id="path9120-7"
style="fill:#ffffff;stroke-width:1.08238"
d="m 117.37921,203.96708 c -0.14178,1.25186 -1.37997,2.0155 -2.73412,1.75184 -1.35547,-0.26392 -2.36714,-1.47747 -2.25687,-2.76839 0.11009,-1.28887 1.30992,-2.13709 2.71099,-1.84239 1.40243,0.29499 2.42203,1.60485 2.28,2.85894 z"
inkscape:original-d="m 114.515,206.85796 a 2.2541561,2.202522 0 0 1 -2.25416,2.20252 2.2541561,2.202522 0 0 1 -2.25415,-2.20252 2.2541561,2.202522 0 0 1 2.25415,-2.20253 2.2541561,2.202522 0 0 1 2.25416,2.20253 z" />
<path
id="path12313"
style="fill:#f2690d;stroke-width:0.564999;fill-opacity:1"
d="m 59.61672,154.18744 c 0.325151,2.25865 -1.094074,4.33249 -3.15765,4.75924 -2.066541,0.42736 -3.927132,-0.95038 -4.182199,-3.16042 -0.255049,-2.20991 1.15468,-4.47943 3.124149,-4.93113 1.973014,-0.45251 3.890438,1.07292 4.2157,3.33231 z"
inkscape:original-d="m 63.545413,162.01134 a 3.3659053,3.4005971 0 0 1 -3.365905,3.40059 3.3659053,3.4005971 0 0 1 -3.365905,-3.40059 3.3659053,3.4005971 0 0 1 3.365905,-3.4006 3.3659053,3.4005971 0 0 1 3.365905,3.4006 z" />
<path
id="path12313-5"
style="fill:#f2690d;stroke-width:0.564999;fill-opacity:1"
d="m 100.67583,145.34781 c 0.68972,2.62689 -0.737048,4.55747 -2.929128,4.52984 -2.199673,-0.0277 -4.352575,-1.98806 -5.023466,-4.53092 -0.66924,-2.5366 0.35871,-4.79944 2.53794,-4.83575 2.187455,-0.0364 4.723132,2.2031 5.414654,4.83683 z"
inkscape:original-d="m 102.14055,158.62677 a 3.3659053,3.4005971 0 0 1 -3.365907,3.4006 3.3659053,3.4005971 0 0 1 -3.365905,-3.4006 3.3659053,3.4005971 0 0 1 3.365905,-3.4006 3.3659053,3.4005971 0 0 1 3.365907,3.4006 z" />
<path
id="rect12882"
style="fill:#f2690d;fill-rule:evenodd;stroke-width:0.268226;fill-opacity:1"
transform="rotate(-42.226221)"
d="m -62.335428,154.54077 c 0.66808,-0.13513 1.263954,0.27377 1.341549,0.92584 0.80958,6.8033 1.492303,13.5175 2.000238,20.24533 0.04866,0.64459 -0.42698,1.21541 -1.063536,1.28056 -0.63649,0.0652 -1.189609,-0.39795 -1.240323,-1.03366 -0.53133,-6.66033 -1.244666,-13.30207 -2.09722,-20.01968 -0.08136,-0.64105 0.391019,-1.26321 1.059292,-1.39839 z"
inkscape:original-d="m -64.260151,162.40195 c 0.571213,0 1.031071,0.4726 1.031071,1.05966 v 18.384 c 0,0.58705 -0.459858,1.05966 -1.031071,1.05966 -0.571213,0 -1.031071,-0.47261 -1.031071,-1.05966 v -18.384 c 0,-0.58706 0.459858,-1.05966 1.031071,-1.05966 z" />
<path
id="rect12882-3"
style="fill:#f2690d;fill-rule:evenodd;stroke-width:0.268226;fill-opacity:1"
transform="rotate(41.818678)"
d="m 169.51646,45.157992 c 0.81318,-0.03527 1.59362,0.464793 1.7419,1.1174 1.54292,6.790599 2.5836,13.657492 3.34606,20.447674 0.0732,0.652269 -0.39854,1.202218 -1.04961,1.234592 -0.65114,0.03238 -1.24268,-0.465344 -1.32641,-1.119211 -0.87115,-6.803429 -2.0746,-13.662344 -3.86252,-20.438936 -0.17174,-0.650948 0.33741,-1.206244 1.15058,-1.241519 z"
inkscape:original-d="m 179.57863,53.562336 c 0.57122,0 1.03107,0.472608 1.03107,1.059659 v 18.384006 c 0,0.587052 -0.45985,1.059659 -1.03107,1.059659 -0.57121,0 -1.03107,-0.472607 -1.03107,-1.059659 V 54.621995 c 0,-0.587051 0.45986,-1.059659 1.03107,-1.059659 z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
docs/screenshots/drawer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

BIN
docs/screenshots/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

BIN
docs/screenshots/player.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
docs/screenshots/setup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB