# Live Diashow Concept

> **Status: SHIPPED.** Implementation lives at
> [frontend/src/lib/diashow/](../frontend/src/lib/diashow/) and
> [frontend/src/routes/diashow/+page.svelte](../frontend/src/routes/diashow/+page.svelte).
> Treat this doc as the design reference; code is the source of truth.

## Goal

A fullscreen, auto-advancing slideshow that any user can start from their device. Suitable
for a venue projector or TV running off a single phone/laptop. Two behaviours combine in one
view:

1. **Live drain** — when a new post is uploaded mid-event, it appears on the next slide
   transition.
2. **Shuffle fallback** — between new uploads (and they will be rare in quiet stretches), the
   diashow rotates through everything posted so far, in shuffled order.

The user does **not** need to be Host or Admin. Any guest can start the diashow on their own
device. There is no global "the room's diashow" — each device runs its own (though they will
look very similar if started at the same time).

---

## Behavioural model

Two FIFO queues live in the client:

| Queue          | Source                                    | Drain priority |
|----------------|-------------------------------------------|----------------|
| `liveQueue`    | SSE events for new processed uploads      | First          |
| `shuffleQueue` | Snapshot of all known uploads, shuffled   | When live empty |

### Slide-advance algorithm

```ts
function nextSlide(): Slide | null {
  // 1. Drain live posts first (FIFO).
  if (liveQueue.length) return liveQueue.shift()!;

  // 2. Refill shuffle queue from `allKnown` if drained.
  if (!shuffleQueue.length) {
    shuffleQueue = shuffle(
      [...allKnown.values()].filter(s => !recentlyShown.has(s.id))
    );
  }
  return shuffleQueue.shift() ?? null;
}
```

A small ring buffer `recentlyShown` (last ~5 IDs) prevents the same picture coming back
within seconds when the shuffle queue is rebuilt.

### Live insertion

```ts
sseClient.on('upload-processed', (msg) => {
  if (allKnown.has(msg.upload_id)) return;
  const slide = await fetchUpload(msg.upload_id); // or use payload directly
  allKnown.set(slide.id, slide);
  liveQueue.push(slide);
});
```

Listen on **`upload-processed`**, not `new-upload` — the preview/thumbnail must exist before
we try to display the slide.

### Deletion / hiding

```ts
sseClient.on('upload-deleted', ({ upload_id }) => {
  allKnown.delete(upload_id);
  liveQueue   = liveQueue.filter(s => s.id !== upload_id);
  shuffleQueue = shuffleQueue.filter(s => s.id !== upload_id);
  if (currentSlide?.id === upload_id) advanceImmediately();
});
```

Hidden uploads (banned user with `uploads_hidden=true`) need either a new SSE event or to
piggyback `upload-deleted` for diashow purposes. Simplest path: emit `upload-deleted` for
hidden posts to all subscribers (the live feed already filters them via `v_feed`, so this is
a backwards-compatible signal).

---

## Initial pool

On start:

1. Call `GET /api/v1/feed?limit=200` (or paginate-and-drain in the background while the
   diashow runs).
2. Push every returned upload into `allKnown`.
3. Build the first `shuffleQueue` from it.
4. Open the SSE stream and route `upload-processed` / `upload-deleted` into the queues.

If the event is empty, show a friendly placeholder:
*"Noch keine Beiträge — neue erscheinen automatisch."*

---

## Frontend surface

### Entry point

A small **Diashow / "Präsentation starten"** action visible:

- In the feed header (icon next to the list/grid toggle) on tablet/desktop layouts.
- In the Account page on mobile (less prominent — diashow is primarily a venue-screen
  feature).

Tapping it navigates to the `/diashow` route (full-screen, bottom nav hidden).

### Route: `/diashow`

- Fullscreen request via `element.requestFullscreen()` after first user gesture.
- **Screen Wake Lock**: `navigator.wakeLock.request('screen')` to keep the screen on during
  long shows. Renew on `visibilitychange` if needed.
- Default dwell: **6 seconds** per slide. Configurable via overlay control: 3 / 6 / 10 s.
- Tap or `Escape` reveals an overlay with: pause/resume, dwell selector, **transition
  picker**, exit.
- Transitions: crossfade (≈400 ms) by default; Ken Burns, zoom, slide, etc. available as
  pluggable components — see "Pluggable transitions" below.
- Videos: autoplay muted, fit-to-screen, advance on `ended` or after `max(dwell, 12 s)`,
  whichever first.
- Preload the next slide's media into a hidden `<img>`/`<video>` to avoid flashing.
- **Media source respects the user's data mode**
  (see [FEATURES.md §2.5](FEATURES.md)). In Saver mode the diashow loads `preview_url`;
  in Original mode it loads the original. The data-usage warning is shown once when the
  mode is toggled in My Account — the diashow itself stays silent.

### Pluggable transitions

Each transition is a **drop-in Svelte component** under
`frontend/src/lib/diashow/transitions/` (path finalised at implementation time). The
interface is intentionally tiny:

```ts
// pseudocode — the real shape lands with the feature
export interface SlideTransition {
  id: string;                  // 'crossfade', 'kenburns', ...
  label: string;               // shown in the dwell/transition picker
  durationMs: number;          // default; can be overridden per-event
  // The actual motion is implemented by mounting the component with `from` / `to` slides.
}
```

A small registry maps `id → component`; the settings popover renders that registry as a
dropdown. **Adding a new animation is one new file plus one line in the registry — no
other changes required.** This is the maintainability target called out in
[FEATURES.md §2.9](FEATURES.md) and [IDEAS.md](IDEAS.md) ("Animation pack").

The same pattern is a candidate for whole-event "themes" later — a bundle of (transition
+ dwell + optional background-music defaults).

### Edge cases

| Case                                       | Behaviour                                              |
|--------------------------------------------|--------------------------------------------------------|
| Empty event                                | Placeholder card; live SSE will trigger the first show |
| All known uploads are still compressing    | Same placeholder — wait for `upload-processed`         |
| Network drop / SSE reconnect               | EventSource auto-reconnects; queues survive            |
| Current slide gets deleted                 | Advance immediately                                    |
| Event is closed (no new uploads possible)  | Diashow keeps running on shuffle queue indefinitely    |
| Banned user's content (`uploads_hidden`)   | Removed via `upload-deleted` signal (see Deletion)     |

---

## Backend changes

**Essentially none.** The diashow reuses:

- `GET /api/v1/feed` (initial pool)
- `GET /api/v1/stream` SSE (`upload-processed`, `upload-deleted`)

Optional small additions:

1. Emit `upload-deleted` (or a new `upload-hidden`) when a host bans a user with
   `hide_uploads=true`, so that diashow clients can scrub the relevant slides without
   reloading.
2. Consider raising the cap on `GET /api/v1/feed?limit=` for diashow clients (or paginate
   the initial pool in the background — preferred, no API change needed).

---

## Future extensions (not in scope for v1)

The big ones live in [IDEAS.md](IDEAS.md) under "Diashow extensions" — most notably the
**global / synchronised diashow** where multiple screens share one server-side cursor.
Short list of others kept here for context:

- **Curated highlights mode** — only show uploads tagged with a specific hashtag, or
  Host-pinned "Story" uploads (depends on the story-highlights feature).
- **Audio bed** — host can pick a background track; mute videos so they don't fight the
  music.
- **Slide caption / uploader chyron** — small lower-third with the uploader's name and
  caption. Out by default to keep the visual clean.
