feat(slideshow): db columns for live slideshow

- 137: events.show_share_token + show_interval_ms/transition/transition_ms
- 138: per-event watermark (tri-state, nullable=inherit global), source/
  position/opacity/style + color filter columns, and event_types.slideshow_preset
  JSON so new events inherit a per-type default. Opt-in, no backfill.
This commit is contained in:
Luca
2026-06-19 22:10:48 +02:00
parent 38514ce60f
commit 1029dd05bd
2 changed files with 100 additions and 0 deletions
@@ -0,0 +1,42 @@
const { addColumnIfNotExists, createIndexIfNotExists } = require('../helpers');
// Live Slideshow ("Diashow") link for live events. A SECOND, token-only
// share surface that mirrors the client-access pattern (migration 074):
// a dedicated fullscreen kiosk URL `/gallery/:slug/show/:token` that
// auto-picks-up newly uploaded photos while it runs.
//
// Opt-in by design: `show_share_token` stays NULL until the admin clicks
// "Generate slideshow link", and the public `/show/` route 404s while it
// is null. The three settings columns drive the running projector and can
// be changed LIVE from the admin panel (a short settings poll on the show
// page picks them up within a few seconds) — they carry sensible defaults
// so an existing event is fully configured the moment a token is minted.
exports.up = async function up(knex) {
// Token IS the secret (no gallery password). Unique so a stray collision
// can never point two events at one link.
await addColumnIfNotExists(knex, 'events', 'show_share_token', (table) => {
table.string('show_share_token', 64).nullable().unique();
});
// Per-slide display time in ms (how long each photo stays on screen).
await addColumnIfNotExists(knex, 'events', 'show_interval_ms', (table) => {
table.integer('show_interval_ms').defaultTo(5000);
});
// Transition style between slides: crossfade | cut | slide | kenburns.
await addColumnIfNotExists(knex, 'events', 'show_transition', (table) => {
table.string('show_transition', 20).defaultTo('crossfade');
});
// Transition animation duration in ms (how fast the transition plays).
await addColumnIfNotExists(knex, 'events', 'show_transition_ms', (table) => {
table.integer('show_transition_ms').defaultTo(800);
});
// Lookup is always by token; index it for the public /show/ route.
await createIndexIfNotExists(knex, 'events', ['show_share_token'], 'idx_events_show_share_token');
};
exports.down = async function down() {
// Safe rollback - intentionally no-op to avoid data loss (mirrors 074).
};
@@ -0,0 +1,58 @@
const { addColumnIfNotExists } = require('../helpers');
// Live Slideshow styling (migration 137 follow-up):
// - a ZDF/ARD-ident-style watermark: a white, semi-transparent logo in a
// corner of the projected slideshow (sourced from the site branding logo
// or the event's own logo).
// - a color filter applied to every slide (none / b&w / sepia / warm / cool
// / vignette).
// - per-EVENT-TYPE slideshow presets: a JSON blob on event_types that new
// events of that type seed their slideshow settings from, so an admin sets
// "weddings fade slowly with our white logo, sepia" once.
//
// All opt-in: watermark defaults OFF, colorfilter defaults 'none', and the
// type preset is NULL until configured — existing events/types are unchanged.
exports.up = async function up(knex) {
// --- per-event live styling (seeded from the type preset on create) ---
// Tri-state: NULL = inherit the global default (app_settings
// slideshow_watermark_*), true/false = explicit per-event override.
await addColumnIfNotExists(knex, 'events', 'show_watermark', (table) => {
table.boolean('show_watermark').nullable();
});
// Which logo to overlay: 'logo' (light branding logo) | 'logo_dark' (dark-mode
// branding logo) | 'favicon' | 'event' (event hero logo).
await addColumnIfNotExists(knex, 'events', 'show_watermark_source', (table) => {
table.string('show_watermark_source', 20).defaultTo('logo');
});
// Corner: top-left | top-right | bottom-left | bottom-right.
await addColumnIfNotExists(knex, 'events', 'show_watermark_position', (table) => {
table.string('show_watermark_position', 20).defaultTo('bottom-right');
});
// 0-100; rendered semi-transparent like a TV station ident.
await addColumnIfNotExists(knex, 'events', 'show_watermark_opacity', (table) => {
table.integer('show_watermark_opacity').defaultTo(60);
});
// 'white' = recolor the logo white (for dark/transparent marks, the TV-ident
// look); 'original' = render as-is (for logos with their own filled box /
// colors, e.g. a boxed badge that would otherwise become a white blob).
await addColumnIfNotExists(knex, 'events', 'show_watermark_style', (table) => {
table.string('show_watermark_style', 20).defaultTo('white');
});
// none | bw | sepia | warm | cool | vignette.
await addColumnIfNotExists(knex, 'events', 'show_colorfilter', (table) => {
table.string('show_colorfilter', 20).defaultTo('none');
});
// --- per-event-type slideshow preset (JSON; null = no preset) ---
// Shape: { interval_ms, transition, transition_ms, watermark, watermark_source,
// watermark_position, watermark_opacity, colorfilter }. Single column
// keeps the type table tidy; the create-event path reads it and seeds the new
// event's show_* columns.
await addColumnIfNotExists(knex, 'event_types', 'slideshow_preset', (table) => {
table.text('slideshow_preset').nullable();
});
};
exports.down = async function down() {
// Safe rollback - intentionally no-op to avoid data loss (mirrors 074/137).
};