feat(slideshow): per-event play order + category filter (#202)
The Live Slideshow already covers the core of #202 (fullscreen kiosk, live-appending new uploads, timing/transitions/watermark, per-event opt-in via the share link). This adds the two customization dimensions the reporter also asked for: - **Play order** (show_order): 'chronological' (upload order, default) or 'random' — the client shuffles the initial set (Fisher-Yates) so live-appended uploads keep working. - **Category filter** (show_category_id): restrict the slideshow to a single photo category (NULL = all photos, default). Enforced server-side on the slideshow /photos access and mirrored in the /session + /state photo_count, so the kiosk viewer can't widen the set. Per-event enable/disable (default off) is unchanged — it's the existing 'Generate/Disable slideshow link' flow (no token = no slideshow). - Migration 158: show_order (default 'chronological') + show_category_id. - Admin: Play-order dropdown + category picker in the Live Slideshow card (picker hidden for events without categories); EN + DE i18n. - Verified: migration (SQLite + PG); live API (category filter → 3/2/5 photos + matching count; order propagates) and the running kiosk requests exactly the filtered set; tsc clean, 136 backend tests pass.
This commit is contained in:
@@ -13,6 +13,7 @@ const DEFAULT_SETTINGS: SlideshowSettings = {
|
||||
transition: 'crossfade',
|
||||
transition_ms: 800,
|
||||
colorfilter: 'none',
|
||||
order: 'chronological',
|
||||
fit: 'cover',
|
||||
watermark: null,
|
||||
};
|
||||
@@ -65,6 +66,17 @@ function watermarkCorner(position: string): React.CSSProperties {
|
||||
|
||||
type Phase = 'splash' | 'running' | 'ended';
|
||||
|
||||
// Fisher–Yates shuffle for the 'random' play order (#202). Used once on the
|
||||
// initial photo set; live-appended uploads keep landing at the end.
|
||||
function shuffle<T>(arr: T[]): T[] {
|
||||
const a = [...arr];
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
export function SlideshowPage() {
|
||||
const { slug = '', token = '' } = useParams<{ slug: string; token: string }>();
|
||||
const { t } = useTranslation();
|
||||
@@ -159,13 +171,15 @@ export function SlideshowPage() {
|
||||
storeGalleryToken(slug, session.token);
|
||||
setActiveGallerySlug(slug);
|
||||
setEventName(session.event.event_name || '');
|
||||
setSettings(session.settings || DEFAULT_SETTINGS);
|
||||
const settings = session.settings || DEFAULT_SETTINGS;
|
||||
setSettings(settings);
|
||||
|
||||
// Load the list and DECODE the first slide (and the next) before we flip
|
||||
// to running, so playback starts on an already-rasterised image instead
|
||||
// of struggling on the first transition.
|
||||
const data = await galleryService.getGalleryPhotos(slug);
|
||||
const list = data.photos || [];
|
||||
// 'random' shuffles the initial set once; new uploads still append (#202).
|
||||
const list = settings.order === 'random' ? shuffle(data.photos || []) : (data.photos || []);
|
||||
setPhotos(list);
|
||||
await preloadDecode(list[0]);
|
||||
void preloadDecode(list[1]);
|
||||
|
||||
Reference in New Issue
Block a user