Files
EventSnap/docs/CONCEPT_DIASHOW.md
MechaCat02 9a0ceeced7 docs: realign blueprint with shipped state + add feature/journey/ideas docs
- PROJECT.md, README.md, TEST_GUIDE.md: status line refreshed; rate-limiter
  doc-vs-code drift fixed; HTML export section rewritten for the SvelteKit-
  static viewer; SSE event names + new events documented; config seed block
  extended with planned toggles + privacy_note; decision log entries added.
- docs/CONCEPT_HTML_VIEWER.md, docs/CONCEPT_MOBILE_UI.md: banner the design
  intent as shipped; point at the source-of-truth code paths.
- docs/CONCEPT_DIASHOW.md: planned-then-shipped design for the live diashow
  (two-queue policy, pluggable transitions, data-mode aware).
- docs/FEATURES.md: capability matrix by role (Guest / Host / Admin) plus
  prose per area (auth, posting, feed, moderation, admin, export, gestures,
  data mode, quotas, privacy note, extensibility).
- docs/USER_JOURNEYS.md: step-by-step flows for every supported scenario,
  including PIN reset by host, data mode, privacy note, gestures, and the
  admin toggles.
- docs/IDEAS.md: speculative extensions (global diashow, reactions,
  multi-tenancy, animation pack, etc.) — explicitly out of v0.16 scope.
- backend/migrations/README.md, frontend/src/lib/README.md: codify the
  "never edit a shipped migration" rule and the lib/ conventions
  (one store per concern, gestures via actions, sheets via ContextSheet,
  transitions as drop-in components).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:31:06 +02:00

7.8 KiB

Live Diashow Concept

Status: SHIPPED. Implementation lives at frontend/src/lib/diashow/ and 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

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

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

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). 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:

// 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 and 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 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.