feat(hobbs-organizer): v1.3.0 photo gallery + Shield photo app 1.1, slideshow 15s

- /photo/gallery: D-pad grid, OK opens full screen, arrows browse, Back returns
- 400px gallery tiles cached per photo (sync + uploads)
- com.dayline.photos 1.1: launcher tile opens the gallery, takes remote input,
  Back steps through WebView history; screensaver unchanged
- slideshow interval 20s -> 15s (live settings)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E9NnVEico3WvFxLyjy1iW3
This commit is contained in:
2026-11-04 21:19:24 -06:00
parent 5cfd2de432
commit 00e0cc42cf
16 changed files with 382 additions and 33 deletions
@@ -4,7 +4,7 @@ import { addUpload, isJpeg, removeUpload } from "../../../../_lib/photos";
export const dynamic = "force-dynamic";
/**
* The admin page resizes photos in the browser (max 1920px JPEG plus a 64px thumbnail)
* The admin page resizes photos in the browser (max 1920px JPEG, a 400px gallery tile and a 64px thumbnail)
* before sending them, so the server only validates and stores.
*/
export async function POST(request: Request) {
@@ -23,9 +23,17 @@ export async function POST(request: Request) {
const photoBytes = new Uint8Array(await photo.arrayBuffer());
const thumbBytes = new Uint8Array(await thumb.arrayBuffer());
if (!isJpeg(photoBytes) || !isJpeg(thumbBytes)) return json({ error: "Photos must be JPEG." }, 415);
// The gallery tile is optional (older admin pages don't send one); the gallery then uses the full photo.
const tile = form.get("tile");
let tileBytes: Uint8Array | null = null;
if (tile instanceof Blob) {
if (tile.size > 1024 * 1024) return json({ error: "Photo is too large." }, 413);
tileBytes = new Uint8Array(await tile.arrayBuffer());
if (!isJpeg(tileBytes)) return json({ error: "Photos must be JPEG." }, 415);
}
const takenAt = Number(form.get("takenAt"));
try {
const result = await addUpload(photoBytes, thumbBytes, {
const result = await addUpload(photoBytes, thumbBytes, tileBytes, {
takenAt: Number.isFinite(takenAt) && takenAt > 0 ? takenAt : null,
width: Math.max(0, Math.round(Number(form.get("width")) || 0)),
height: Math.max(0, Math.round(Number(form.get("height")) || 0)),
@@ -15,7 +15,7 @@ export async function GET() {
{
status: "ok",
service: "hobbs-family-organizer",
version: "1.2.0",
version: "1.3.0",
calendarConfigurationValid: calendars.some((calendar) => calendar.enabled),
calendarsEnabled: calendars.filter((calendar) => calendar.enabled).length,
adminConfigured,