diff --git a/backend/migrations/core/137_add_slideshow_share.js b/backend/migrations/core/137_add_slideshow_share.js new file mode 100644 index 00000000..0b06a88d --- /dev/null +++ b/backend/migrations/core/137_add_slideshow_share.js @@ -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). +}; diff --git a/backend/migrations/core/138_add_slideshow_styling.js b/backend/migrations/core/138_add_slideshow_styling.js new file mode 100644 index 00000000..4a159d1e --- /dev/null +++ b/backend/migrations/core/138_add_slideshow_styling.js @@ -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). +};