From b5c73e05bd41b262f864e8c700b1d38582b3817f Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Sat, 20 Jun 2026 12:06:34 +0200 Subject: [PATCH] feat(slideshow): add image fit setting (fill vs black bars) object-fit was hardcoded to 'cover', which crops portrait photos heavily. Add a global `slideshow_fit` setting (Settings -> Slideshow): 'cover' fills + crops, 'contain' shows the whole image with black bars (no crop). Default 'cover' (unchanged). Stored in app_settings (no migration), resolved server-side into the slideshow settings + /state poll so a running projector picks it up live. --- backend/src/routes/adminSettings.js | 3 ++ backend/src/routes/gallery.js | 5 ++++ .../admin/SlideshowGlobalDefaultsCard.tsx | 28 +++++++++++++++++-- frontend/src/i18n/locales/de.json | 10 +++++-- frontend/src/i18n/locales/en.json | 10 +++++-- frontend/src/pages/gallery/SlideshowPage.tsx | 13 +++++---- frontend/src/services/slideshow.service.ts | 7 +++++ 7 files changed, 65 insertions(+), 11 deletions(-) diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js index d6197f44..22a2d843 100644 --- a/backend/src/routes/adminSettings.js +++ b/backend/src/routes/adminSettings.js @@ -299,6 +299,9 @@ router.put('/slideshow', adminAuth, requirePermission('settings.edit'), async (r const push = (key, value) => updates.push({ setting_key: key, setting_value: JSON.stringify(value), setting_type: 'slideshow' }); const has = (k) => Object.prototype.hasOwnProperty.call(req.body, k); + if (has('slideshow_fit')) { + push('slideshow_fit', req.body.slideshow_fit === 'contain' ? 'contain' : 'cover'); + } 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'; diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index f2b7226f..89533e4d 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -301,11 +301,16 @@ async function slideshowSettings(event) { watermark = { url, position: position || 'bottom-right', opacity: opacity ?? 60, style: style || 'white', size: size ?? 12 }; } } + // Image fit is a global setting (Settings → Slideshow): 'cover' fills + crops, + // 'contain' shows the whole image with black bars (no crop). + const fitRaw = await getAppSetting('slideshow_fit', 'cover'); + const fit = fitRaw === 'contain' ? 'contain' : 'cover'; return { interval_ms: event.show_interval_ms || 5000, transition: event.show_transition || 'crossfade', transition_ms: event.show_transition_ms || 800, colorfilter: event.show_colorfilter || 'none', + fit, watermark, }; } diff --git a/frontend/src/components/admin/SlideshowGlobalDefaultsCard.tsx b/frontend/src/components/admin/SlideshowGlobalDefaultsCard.tsx index 5ad544d3..b42b0c39 100644 --- a/frontend/src/components/admin/SlideshowGlobalDefaultsCard.tsx +++ b/frontend/src/components/admin/SlideshowGlobalDefaultsCard.tsx @@ -16,11 +16,13 @@ import { settingsService } from '../../services/settings.service'; import { SLIDESHOW_WATERMARK_POSITIONS, SLIDESHOW_WATERMARK_STYLES, + SLIDESHOW_FITS, type SlideshowGlobalDefaults, } from '../../services/slideshow.service'; import { WatermarkSourcePicker } from './WatermarkSourcePicker'; const DEFAULTS: SlideshowGlobalDefaults = { + slideshow_fit: 'cover', slideshow_watermark_enabled: false, slideshow_watermark_source: 'logo', slideshow_watermark_position: 'bottom-right', @@ -43,6 +45,7 @@ export const SlideshowGlobalDefaultsCard: React.FC = () => { settingsService.getSettingsByType('slideshow').then((s) => { if (cancelled || !s) return; setVal({ + slideshow_fit: s.slideshow_fit ?? DEFAULTS.slideshow_fit, slideshow_watermark_enabled: s.slideshow_watermark_enabled ?? DEFAULTS.slideshow_watermark_enabled, slideshow_watermark_source: s.slideshow_watermark_source ?? DEFAULTS.slideshow_watermark_source, slideshow_watermark_position: s.slideshow_watermark_position ?? DEFAULTS.slideshow_watermark_position, @@ -70,13 +73,33 @@ export const SlideshowGlobalDefaultsCard: React.FC = () => {

- {t('slideshow.globalTitle', 'Global slideshow watermark')} + {t('slideshow.globalTitle', 'Global slideshow settings')}

- {t('slideshow.globalDescription', 'Default logo watermark for every slideshow. Individual events can override this on or off.')} + {t('slideshow.globalDescription', 'Defaults for every slideshow. Events can override the watermark on or off.')}

+ {/* Image fit */} +
+ + +

+ {t('slideshow.fitHint', '"Fill" crops to fill the screen; "Black bars" shows the whole photo — better for portrait images.')} +

+
+ +
)} +