refactor(slideshow): replace per-event-type preset with a picpeak-wide one

The slideshow display preset (transition / interval / speed / color filter) was
set PER EVENT TYPE in the Edit Event Type dialog. Replace it with a single
picpeak-wide default in Settings -> Slideshow ("Default style for new
slideshows"). New events seed their show_* columns from this global preset
(was: from the event type's slideshow_preset); the per-event override is
unchanged.

- Removed event_types.slideshow_preset usage everywhere (EventTypeModal section,
  eventTypes.service types, eventTypeService whitelist, adminEventTypes
  validators/POST). The DB column from migration 138 is left inert.
- Global preset stored in app_settings (slideshow_interval_ms/transition/
  transition_ms/colorfilter), saved via PUT /admin/settings/slideshow.
- adminEvents create-seeding now reads the global preset (getAppSetting) instead
  of the event type.
- en/de: presetTitle + presetHint.
This commit is contained in:
Luca
2026-06-20 12:20:48 +02:00
parent b5c73e05bd
commit 5f1f4c2b3d
10 changed files with 118 additions and 92 deletions
-4
View File
@@ -89,7 +89,6 @@ router.post('/', adminAuth, requirePermission('settings.edit'), [
body('emoji').optional().trim(),
body('theme_preset').optional().trim(),
body('theme_config').optional(),
body('slideshow_preset').optional(),
body('display_order').optional().isInt({ min: 0 })
], async (req, res) => {
try {
@@ -104,7 +103,6 @@ router.post('/', adminAuth, requirePermission('settings.edit'), [
emoji,
theme_preset,
theme_config,
slideshow_preset,
display_order
} = req.body;
@@ -114,7 +112,6 @@ router.post('/', adminAuth, requirePermission('settings.edit'), [
emoji,
theme_preset,
theme_config,
slideshow_preset,
display_order
});
@@ -153,7 +150,6 @@ router.put('/:id', adminAuth, requirePermission('settings.edit'), [
body('emoji').optional().trim(),
body('theme_preset').optional().trim(),
body('theme_config').optional(),
body('slideshow_preset').optional(),
body('display_order').optional().isInt({ min: 0 }),
body('is_active').optional().isBoolean()
], async (req, res) => {
+16 -29
View File
@@ -26,6 +26,7 @@ const { hasColumnCached } = require('../utils/schemaCache');
const { validateFileType } = require('../utils/fileSecurityUtils');
const { requireEventOwnership } = require('../middleware/ownership');
const { requireFeatureFlag } = require('../middleware/requireFeatureFlag');
const { getAppSetting } = require('../utils/appSettings');
const { getFrontendBaseUrl } = require('../utils/frontendUrl');
const downloadZipService = require('../services/downloadZipService');
@@ -673,40 +674,26 @@ router.post('/', adminAuth, requirePermission('events.create'), [
const calendarColumnsExist = await hasColumnCached('events', 'is_full_day');
// Insert into database
// Seed the new event's Live Slideshow settings from the event TYPE's preset
// (migration 138). The admin configures "weddings fade slowly with our
// white logo, sepia" once on the type; every new wedding inherits it. The
// share token is NOT seeded — the link is still minted on demand. Guarded
// Seed the new event's Live Slideshow display style from the PICPEAK-WIDE
// preset (app_settings, Settings → Slideshow). New events inherit it and the
// admin can still override per event. Watermark is left NULL = inherit the
// global watermark; the share token is minted on demand, not seeded. Guarded
// so un-migrated installs (mid-branch) don't reference missing columns.
let slideshowSeed = {};
if (await hasColumnCached('events', 'show_interval_ms')) {
try {
const type = await eventTypeService.getEventTypeBySlugPrefix(event_type);
const preset = type?.slideshow_preset
? (typeof type.slideshow_preset === 'string' ? JSON.parse(type.slideshow_preset) : type.slideshow_preset)
: null;
if (preset && typeof preset === 'object') {
const intP = (v, min, max) => (Number.isFinite(+v) ? Math.min(max, Math.max(min, parseInt(v, 10))) : undefined);
const oneOf = (v, allowed) => (allowed.includes(v) ? v : undefined);
// Tri-state watermark: 'on'/'off' seed an explicit override; 'inherit'
// (or unset) leaves the column NULL so the event follows the global.
let watermarkSeed;
if (preset.watermark === 'on' || preset.watermark === true) watermarkSeed = formatBoolean(true);
else if (preset.watermark === 'off' || preset.watermark === false) watermarkSeed = formatBoolean(false);
const seed = {
show_interval_ms: intP(preset.interval_ms, 1000, 120000),
show_transition: oneOf(preset.transition, SLIDESHOW_TRANSITIONS),
show_transition_ms: intP(preset.transition_ms, 100, 5000),
show_watermark: watermarkSeed,
show_colorfilter: oneOf(preset.colorfilter, SLIDESHOW_COLORFILTERS),
};
// Only carry through fields the preset actually set.
for (const [k, v] of Object.entries(seed)) {
if (v !== undefined) slideshowSeed[k] = v;
}
}
const intP = (v, min, max) => (Number.isFinite(+v) ? Math.min(max, Math.max(min, parseInt(v, 10))) : undefined);
const oneOf = (v, allowed) => (allowed.includes(v) ? v : undefined);
const i = intP(await getAppSetting('slideshow_interval_ms', undefined), 1000, 120000);
const tr = oneOf(await getAppSetting('slideshow_transition', undefined), SLIDESHOW_TRANSITIONS);
const tms = intP(await getAppSetting('slideshow_transition_ms', undefined), 100, 5000);
const cf = oneOf(await getAppSetting('slideshow_colorfilter', undefined), SLIDESHOW_COLORFILTERS);
if (i !== undefined) slideshowSeed.show_interval_ms = i;
if (tr) slideshowSeed.show_transition = tr;
if (tms !== undefined) slideshowSeed.show_transition_ms = tms;
if (cf) slideshowSeed.show_colorfilter = cf;
} catch (e) {
logger.warn('Failed to seed slideshow settings from event type preset', { error: e.message });
logger.warn('Failed to seed slideshow settings from global preset', { error: e.message });
}
}
+17
View File
@@ -302,6 +302,23 @@ router.put('/slideshow', adminAuth, requirePermission('settings.edit'), async (r
if (has('slideshow_fit')) {
push('slideshow_fit', req.body.slideshow_fit === 'contain' ? 'contain' : 'cover');
}
// Picpeak-wide display preset (default style new events inherit).
if (has('slideshow_interval_ms')) {
const n = Math.min(120000, Math.max(1000, Math.round(Number(req.body.slideshow_interval_ms) || 5000)));
push('slideshow_interval_ms', n);
}
if (has('slideshow_transition')) {
const allowed = ['crossfade', 'cut', 'slide', 'kenburns', 'dipwhite', 'dipblack'];
push('slideshow_transition', allowed.includes(req.body.slideshow_transition) ? req.body.slideshow_transition : 'crossfade');
}
if (has('slideshow_transition_ms')) {
const n = Math.min(5000, Math.max(100, Math.round(Number(req.body.slideshow_transition_ms) || 800)));
push('slideshow_transition_ms', n);
}
if (has('slideshow_colorfilter')) {
const allowed = ['none', 'bw', 'sepia', 'warm', 'cool', 'vignette'];
push('slideshow_colorfilter', allowed.includes(req.body.slideshow_colorfilter) ? req.body.slideshow_colorfilter : 'none');
}
if (has('slideshow_watermark_enabled')) push('slideshow_watermark_enabled', !!req.body.slideshow_watermark_enabled);
if (has('slideshow_watermark_source')) {
const v = ['logo', 'logo_dark', 'favicon', 'event'].includes(req.body.slideshow_watermark_source) ? req.body.slideshow_watermark_source : 'logo';
+1 -7
View File
@@ -100,8 +100,7 @@ const createEventType = async (eventTypeData) => {
emoji,
theme_preset,
theme_config,
display_order,
slideshow_preset
display_order
} = eventTypeData;
// Normalize slug_prefix
@@ -128,7 +127,6 @@ const createEventType = async (eventTypeData) => {
emoji: emoji || '📷',
theme_preset: theme_preset || 'default',
theme_config: theme_config ? JSON.stringify(theme_config) : null,
slideshow_preset: slideshow_preset ? JSON.stringify(slideshow_preset) : null,
display_order: finalDisplayOrder,
is_system: false,
is_active: true,
@@ -193,10 +191,6 @@ const updateEventType = async (id, updates) => {
updateData.theme_config = updates.theme_config ? JSON.stringify(updates.theme_config) : null;
}
if (updates.slideshow_preset !== undefined) {
updateData.slideshow_preset = updates.slideshow_preset ? JSON.stringify(updates.slideshow_preset) : null;
}
if (updates.display_order !== undefined) {
updateData.display_order = updates.display_order;
}