docs: flashier README with screenshots

Restyle the README: centered hero header, a screenshot row (timeline,
Places map, server/backup), and an emoji feature list. Refresh the feature
set and controls for what shipped since (smart search, the Places map +
cluster gallery, analog-stick map control) and link the releases page.
Screenshots live in docs/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uKJQMYDE9Nrq9ECsU3MxH
This commit is contained in:
Sads
2026-06-18 23:31:06 +02:00
co-authored by Claude Opus 4.8
parent b6eb700248
commit f4cf235c40
4 changed files with 120 additions and 114 deletions
+117 -111
View File
@@ -1,151 +1,157 @@
# vitaImmich <div align="center">
Minimal proof-of-concept [Immich](https://immich.app/) client for the PS Vita. # 📷 vitaImmich
It connects to your Immich server and shows your library (photos and videos)
as a scrollable chronological grid (newest first), with a full-screen viewer
and hardware-accelerated video playback.
## Requirements ### Your self-hosted photo library, in your hands.
- A PS Vita with HENkaku/h-encore and VitaShell A native **[Immich](https://immich.app/)** client for the **PlayStation Vita** —
- An Immich server reachable from the Vita's Wi-Fi (recent Immich version — browse, search and map your whole photo & video library over Wi‑Fi, play videos
it uses `POST /api/search/random` and `GET /api/assets/{id}/thumbnail`) on the Vita's hardware decoder, and back up the camera roll, all on a 12‑year‑old
- An Immich API key (web UI → Account Settings → API Keys) handheld.
## Building </div>
Needs [VitaSDK](https://vitasdk.org/) with the `libvita2d`, `curl`, `openssl`, <table>
`zstd`, `zlib`, `libpng`, `libjpeg-turbo` and `freetype` packages installed <tr>
(via `vdpm`). <td width="33%"><img src="docs/timeline.png" alt="Timeline and smart search"></td>
<td width="33%"><img src="docs/places.png" alt="Geotag Places map"></td>
<td width="33%"><img src="docs/backup.png" alt="Server and backup overview"></td>
</tr>
<tr>
<td align="center"><b>Timeline &amp; search</b></td>
<td align="center"><b>Places map</b></td>
<td align="center"><b>Server &amp; backup</b></td>
</tr>
</table>
```sh ---
export VITASDK=$HOME/vitasdk
export PATH=$VITASDK/bin:$PATH
cmake -B build -S .
cmake --build build
```
This produces `build/vitaImmich.vpk`. ## ✨ Features
## Installing & configuring - 🗓️ **Timeline** — your entire library as a justified, date‑grouped grid (newest first), styled after the Immich web app.
- 🔍 **Smart search** — free‑text CLIP search straight from the Vita's keyboard.
- 🗺️ **Places map** — geotagged photos clustered on a world map; pan/zoom with the sticks and open any cluster as its own gallery.
- 🖼️ **Full‑screen viewer** — pinch/stick zoom & pan, swipe between photos, hardware‑accelerated **H.264 video** playback.
- ☁️ **Camera‑roll backup** — scans the Vita's photos & videos, dedupes against the server, and uploads on request.
- 📊 **Server overview** — storage usage, server/app versions, and per‑device backup status.
- 👆 **Touch + buttons** — the whole UI is drivable with the front touchscreen or the d‑pad/sticks.
1. Copy `vitaImmich.vpk` to the Vita and install it with VitaShell. > **Status:** proof‑of‑concept. App title id **`VIMM00001`**. See [limitations](#-limitations).
2. Launch it once — it creates `ux0:data/vitaimmich/config.txt`.
3. Edit that file (VitaShell → SELECT for FTP makes this easy):
``` ## 📦 Requirements
- A PS Vita running **HENkaku/h‑encore** with **VitaShell**.
- A reasonably recent **Immich server** reachable over the Vita's Wi‑Fi.
- An **Immich API key** (web UI → *Account Settings → API Keys*), or an email/password.
## 🚀 Install & configure
1. Grab `vitaImmich.vpk` from the [latest release](https://codeberg.org/SadsArches/vitaImmich/releases) (or build it — see below) and install it with **VitaShell**.
2. Launch it once; it creates `ux0:data/vitaimmich/config.txt`.
3. Edit that file (VitaShell's FTP, **SELECT**, makes this easy):
```ini
server=http://192.168.1.100:2283 server=http://192.168.1.100:2283
apikey=your-immich-api-key apikey=your-immich-api-key
``` ```
If your server sits behind a public domain (reverse proxy) and your Behind a reverse proxy without NAT loopback? Pin the domain to your LAN IP
router does not support NAT loopback, add the server's LAN IP so the so the Host header / TLS SNI stay on the domain but the connection goes
domain is pinned to it while on your home network: straight to the server:
``` ```ini
server=https://immich.example.com server=https://immich.example.com
apikey=your-immich-api-key apikey=your-immich-api-key
serverip=192.168.1.100 serverip=192.168.1.100
``` ```
This keeps the Host header and TLS SNI on the domain (so the reverse 4. Relaunch.
proxy still routes correctly) but connects to the LAN IP directly.
4. Relaunch the app. ## 🎮 Controls
## Controls **Timeline**
Grid view:
| Button | Action | | Button | Action |
| ------------- | ------------------------------- | | --- | --- |
| D-pad | Move selection (hold to repeat) | | D‑pad / sticks | Move selection (hold to repeat) |
| L / R | Jump two rows up / down | | L / R | Jump a month up / down |
| X | Open item full screen | | △ | Smart search |
| SELECT | Open the sync overview | | X | Open full screen |
| 🗺️ / ☁️ (touch) | Open the Places map / server page |
| SELECT | Sync overview |
| START | Exit | | START | Exit |
Full-screen view: **Places map**
| Button | Action | | Button | Action |
| ------------- | --------------------------------- | | --- | --- |
| Left / Right | Previous / next item | | Left stick / d‑pad / drag | Pan |
| X | Play video / retry a failed photo | | Right stick / L · R | Zoom out / in |
| O | Back to grid | | X | Open the centred cluster as a gallery |
| SELECT | Open the sync overview | | △ | Toggle the lat/lon graticule |
| START | Exit | | O | Back |
Sync overview (SELECT): **Full‑screen viewer**
| Button | Action | | Button | Action |
| ------------- | ----------------------------------- | | --- | --- |
| X | Queue all local-only files to upload| | ← / → | Previous / next |
| O / SELECT | Back | | Right stick / L · R | Zoom · left stick pans |
| START | Exit | | X | Play video / retry a failed load |
| O | Back |
Video playback: **Video playback** — X pause/resume · ←/→ seek ±10 s · O stop.
| Button | Action | ## 🔧 Building
| ------------- | ----------------- |
| X | Pause / resume |
| Left / Right | Seek ±10 seconds |
| O | Stop and go back |
More of the library is fetched automatically (100 photos at a time, newest Needs [VitaSDK](https://vitasdk.org/) with `libvita2d`, `curl`, `openssl`,
first) as you scroll toward the end. Errors are shown on screen and logged `zstd`, `zlib`, `libpng`, `libjpeg-turbo` and `freetype` (install via `vdpm`).
to `ux0:data/vitaimmich/log.txt`.
## Syncing the Vita's camera media ```sh
export VITASDK=$HOME/vitasdk
export PATH=$VITASDK/bin:$PATH
cmake -B build -S .
cmake --build build # -> build/vitaImmich.vpk
```
On startup the app mounts the Photos app's storage (`photo0:`, i.e. the The sources in `src/` are a **unity build**: `main.c` `#include`s the section
ACL-protected `ux0:picture` where camera shots and recordings live) via files (`state.c`, `draw.c`, … `app.c`) into one translation unit, so only
`sceAppMgrAppDataMount`, scans it (plus `ux0:video/CAMERA`, a few levels `main.c` is compiled. The geotag map's base tiles live in `assets/maptiles.pak`,
deep; `.jpg/.jpeg/.png/.mp4`) and merges those files into the same generated from a world‑map image by `tools/png_to_tiles.py`.
date-sorted grid as your server library.
Files larger than 512 MB are skipped so a movie collection in `ux0:video`
is never hashed or offered for upload. Both knobs live in `config.txt`:
`syncdir=<folder>` (repeatable, replaces the default scan locations) and
`syncmaxmb=<MB>` (the size cap; 0 disables it).
A small badge in the top-left corner of each cell shows its status:
- blue-grey dot — **cloud only** (a server asset not present on the Vita) ## ☁️ Camera‑roll backup
- orange up-arrow — **local only** (on the Vita, not yet on the server)
- blinking orange — **queued / uploading**
- green check — **backed up** (exists on both; shown once, as the server asset)
- red dot — **failed** (see the sync overview for the reason)
A background thread hashes each local file (SHA1) and asks the server which On startup the app mounts the Photos app's storage (`photo0:` /
ones already exist (`POST /api/assets/bulk-upload-check`). Hashing and the `ux0:picture`) plus `ux0:video/CAMERA`, scans for `.jpg/.jpeg/.png/.mp4`, and
duplicate check start automatically. Uploads do **not** start on their own: merges those files into the same date‑sorted timeline as your server library. A
open the sync overview with **SELECT** and press **X** to queue every badge in each cell's corner shows status:
local-only file. Each upload is a `POST /api/assets` multipart request with
an `x-immich-checksum` header for fast server-side dedup; progress is shown
in the overview.
## PoC limitations - 🔵 blue‑grey dot — **cloud only**
- 🔶 orange up‑arrow — **local only**
- 🟠 blinking — **queued / uploading**
- 🟢 green check — **backed up** (on both; shown once, as the server asset)
- 🔴 red dot — **failed**
- TLS certificate verification is disabled (no CA bundle is shipped), so A background thread SHA‑1‑hashes each local file and asks the server which
prefer plain HTTP on a trusted LAN or treat HTTPS as unverified. already exist (`bulk-upload-check`). Uploads never start on their own — open the
- Thumbnails stream in on a background thread (one at a time over a sync overview (**SELECT**) and press **X** to queue every local‑only file.
keep-alive connection); cells show a grey placeholder until loaded. Tunables in `config.txt`: `syncdir=<folder>` (repeatable) and `syncmaxmb=<MB>`
- Videos are downloaded in full to `ux0:data/vitaimmich/video.mp4` before (size cap; files over 512 MB are skipped by default, 0 disables).
playback (deleted afterwards), so they need free space on the memory card
and a moment to start. Playback uses the Vita's hardware decoder via ## ⚠️ Limitations
SceAvPlayer, so only MP4 (H.264/AAC) plays — Immich's transcoded
`video/playback` stream is used, which is H.264 with default server - **TLS verification is disabled** (no CA bundle ships) — prefer plain HTTP on a
settings. The decoder rotates four output frame buffers (~13 MB CDRAM at trusted LAN, or treat HTTPS as unverified.
1080p) so async GPU rendering can't be torn by a recycled frame. - Videos download in full to the memory card before playing (deleted after).
- Caps at 1000 server assets and 500 local camera files per launch; no Playback is the Vita's hardware decoder via SceAvPlayer, so only **MP4
albums or search. (H.264/AAC)** plays — Immich's transcoded `video/playback` stream is used.
- Local media sync is one-way (Vita → server). It scans `ux0:picture` and - Backup is **one‑way** (Vita → server); `deviceId` is hard‑coded to "PS Vita"
`ux0:video/CAMERA` by default (the standard camera locations), recursing and uploads aren't added to an album.
~2 levels; use `syncdir=` in the config to scan other folders. - Thumbnails stream in one at a time on a background thread (grey placeholder
- Local video grid cells show a poster thumbnail (with the VIDEO badge): until loaded); hashing the camera roll fills statuses in gradually.
the first frame is decoded with SceAvPlayer on the background loader
thread (NV12 -> RGB, downscaled), the same handoff as photos. Poster ---
extraction and playback share the single hardware decoder, so a poster is
never decoded while a video is playing (and vice versa). <div align="center">
- Hashing 500 files at startup takes a while; it runs in the background so Made by <b>SadsArches</b> with love. ❤️
browsing is never blocked, but statuses fill in gradually. </div>
- `deviceId` is hard-coded to "PS Vita" and uploads are not associated with
an album.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB