- 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>
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:
- Live drain — when a new post is uploaded mid-event, it appears on the next slide transition.
- 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:
- Call
GET /api/v1/feed?limit=200(or paginate-and-drain in the background while the diashow runs). - Push every returned upload into
allKnown. - Build the first
shuffleQueuefrom it. - Open the SSE stream and route
upload-processed/upload-deletedinto 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 onvisibilitychangeif needed. - Default dwell: 6 seconds per slide. Configurable via overlay control: 3 / 6 / 10 s.
- Tap or
Escapereveals 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
endedor aftermax(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/streamSSE (upload-processed,upload-deleted)
Optional small additions:
- Emit
upload-deleted(or a newupload-hidden) when a host bans a user withhide_uploads=true, so that diashow clients can scrub the relevant slides without reloading. - 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.