diff --git a/assets/sass/components/_viewer.scss b/assets/sass/components/_viewer.scss new file mode 100644 index 000000000..5a5058040 --- /dev/null +++ b/assets/sass/components/_viewer.scss @@ -0,0 +1,152 @@ +// Standalone viewer page (view.php) + +:root { + // mirror gallery style tokens + --viewer-background: var(--primary-light-color); + --viewer-foreground: var(--font-color, #111); + --viewer-surface: #ffffff; + --viewer-accent: var(--secondary-color, #444); + --viewer-accent-foreground: var(--secondary-font-color, #fff); + --viewer-shadow: 0 18px 48px rgba(0, 0, 0, 0.25); +} + +.viewer-page { + margin: 0; + min-height: 100vh; + background: + radial-gradient(circle at 20% 20%, color-mix(in srgb, var(--viewer-background), white 10%), transparent 32%), + radial-gradient(circle at 85% 10%, color-mix(in srgb, var(--primary-color, #2196f3), white 12%), transparent 28%), + linear-gradient( + 135deg, + color-mix(in srgb, var(--viewer-background), var(--primary-color, #2196f3) 35%), + var(--primary-color, #2196f3) + ); + font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; + color: var(--viewer-foreground); + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} + +.viewer { + width: min(960px, 100%); + background: rgba(255, 255, 255, 0.12); + backdrop-filter: blur(12px); + border-radius: 18px; + box-shadow: var(--viewer-shadow); + padding: clamp(16px, 3vw, 24px); + border: 1px solid rgba(255, 255, 255, 0.12); +} + +.viewer__inner { + background: var(--viewer-surface); + border-radius: 14px; + padding: clamp(14px, 3vw, 22px); + display: flex; + flex-direction: column; + gap: 16px; + border: 1px solid rgba(0, 0, 0, 0.05); +} + +.viewer__header { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.viewer__title { + margin: 0; + width: 100%; + text-align: center; + display: flex; + flex-direction: column; + gap: 0.2em; +} + +.viewer__title-line { + font-size: clamp(22px, 5vw, 34px); + color: var(--viewer-accent); + line-height: 1.05; + font-weight: 700; + letter-spacing: 0.01em; +} + +.viewer__accent { + height: 6px; + width: 100%; + border-radius: 999px; + background: linear-gradient(90deg, var(--primary-color, #2196f3), var(--secondary-color, #3f51b5)); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15); +} + +.viewer__badge { + background: var(--viewer-accent); + color: var(--viewer-accent-foreground); + padding: 6px 10px; + border-radius: 999px; + font-size: 12px; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.viewer__media { + position: relative; + border-radius: 12px; + overflow: hidden; + background: #f5f5f5; + border: 2px solid rgba(0, 0, 0, 0.05); + max-height: 70vh; + display: grid; + place-items: center; + box-shadow: inset 0 12px 22px rgba(0, 0, 0, 0.04); +} + +.viewer__media img, +.viewer__media video { + display: block; + width: 100%; + height: auto; + object-fit: contain; +} + +.viewer__media video { + background: #000; +} + +.viewer__btn { + min-height: 56px; + touch-action: manipulation; + user-select: none; + -webkit-tap-highlight-color: transparent; + padding-inline: 2.2rem; + width: 100%; +} + +.viewer__tip { + margin: 0; + font-size: 14px; + color: rgba(0, 0, 0, 0.64); + text-align: center; +} + +@media (min-width: 720px) { + .viewer__actions { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 540px) { + .viewer { + padding: 12px; + } + + .viewer__inner { + padding: 14px; + } + + .viewer__title { + font-size: clamp(18px, 6vw, 26px); + } +} diff --git a/assets/sass/framework.scss b/assets/sass/framework.scss index 82d7a8355..fefb61d1f 100644 --- a/assets/sass/framework.scss +++ b/assets/sass/framework.scss @@ -23,6 +23,7 @@ @use 'components/virtualKeyboard'; @use 'components/github-corner'; @use 'components/background'; +@use 'components/viewer'; // Experiments @use 'experiments/video-capture-animation'; diff --git a/docs/faq/index.md b/docs/faq/index.md index 741c10403..4d060f8a6 100644 --- a/docs/faq/index.md +++ b/docs/faq/index.md @@ -479,6 +479,13 @@ sudo -u www-data scp /var/www/html/data/images/20230129_125148.jpg [username@rem You can now use the URL with which you can access your remote server from the internet and paste it into the QR code field in the Photobox admin panel. Now using the QR code your pictures can be downloaded from your remote server. +## How do I use QR codes for downloads? + +- Touch-friendly viewer page: `view.php?image=` shows the photo/video with a large download button. +- Direct file download (no UI): `api/download.php?image=`. +- Set the QR target in the admin config under `qr[url]`; a good default is `view.php?image=` so guests open the viewer after scanning. +- Network reminder: guests must reach the URL in the QR. Either put them on the same Wi-Fi/LAN as the Photobooth (no internet needed) or point the QR to a public endpoint that serves the image. + ## How to use the image randomizer To use the image randomizer images must be placed inside `private/images/{folderName}`. diff --git a/resources/lang/de.json b/resources/lang/de.json index d596c688a..b5875aba3 100644 --- a/resources/lang/de.json +++ b/resources/lang/de.json @@ -853,6 +853,9 @@ "qr:qr_text": "Eigener Hilfetext", "qr:qr_url": "URL für QR-Code", "qrHelp": "Um das Bild auf Ihr Handy herunterzuladen, verbinden Sie sich mit dem WLAN:", + "share": "Teilen", + "viewer_photo_title": "Dein Foto", + "viewer_video_fallback": "Dein Browser kann dieses Video nicht abspielen.", "really_delete": "Wirklich nach Ihren Einstellungen zurücksetzen? Dies kann nicht rückgängig gemacht werden!", "really_delete_image": "wird gelöscht! Dies kann nicht rückgängig gemacht werden! Bild wirklich löschen?", "reboot_button": "Neustart", diff --git a/resources/lang/en.json b/resources/lang/en.json index 31913fb9d..0c5ad4e19 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -855,6 +855,9 @@ "qr:qr_text": "Own help text", "qr:qr_url": "URL for QR Code", "qrHelp": "To download the picture to your smartphone, connect to the WiFi:", + "share": "Share", + "viewer_photo_title": "Your photo", + "viewer_video_fallback": "Your browser can’t play this video.", "really_delete": "Really reset according to your settings? This cannot be undone!", "really_delete_image": "will be deleted! This cannot be undone! Really delete picture?", "reboot_button": "Reboot", diff --git a/src/Service/ConfigurationService.php b/src/Service/ConfigurationService.php index 9446775a2..485ecbac8 100644 --- a/src/Service/ConfigurationService.php +++ b/src/Service/ConfigurationService.php @@ -120,7 +120,7 @@ protected function addDefaults(array $config): array } if (empty($config['qr']['url'])) { - $config['qr']['url'] = 'api/download.php?image='; + $config['qr']['url'] = 'view.php?image='; } return $config; diff --git a/view.php b/view.php new file mode 100644 index 000000000..682c8bbd8 --- /dev/null +++ b/view.php @@ -0,0 +1,82 @@ +absolute() . DIRECTORY_SEPARATOR . $image; +if (!is_file($imagePath)) { + http_response_code(404); + echo 'Image not found.'; + exit(); +} + +$extension = strtolower(pathinfo($imagePath, PATHINFO_EXTENSION)); +$isVideo = in_array($extension, ['mp4', 'mov', 'webm'], true); +$mime = match ($extension) { + 'png' => 'image/png', + 'gif' => 'image/gif', + default => 'image/jpeg', +}; +$imageUrl = PathUtility::getPublicPath(FolderEnum::IMAGES->value . '/' . rawurlencode($image)); +$downloadUrl = PathUtility::getPublicPath('api/download.php?image=' . rawurlencode($image)); +$languageService = LanguageService::getInstance(); +$pageTitle = ApplicationService::getInstance()->getTitle() . ' - ' . $languageService->translate('viewer_photo_title'); +$photoswipe = false; +$remoteBuzzer = false; + +include PathUtility::getAbsolutePath('template/components/main.head.php'); +?> + +
+
+
+
+ + + + + + + + + + getTitle()) ?> + +
+
+
+ +
+ + + + Captured photo + +
+ +
+ 'download']) ?> +
+ +
+
+ + + +