Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions assets/sass/components/_viewer.scss
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions assets/sass/framework.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
@use 'components/virtualKeyboard';
@use 'components/github-corner';
@use 'components/background';
@use 'components/viewer';

// Experiments
@use 'experiments/video-capture-animation';
Expand Down
7 changes: 7 additions & 0 deletions docs/faq/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<filename>` shows the photo/video with a large download button.
- Direct file download (no UI): `api/download.php?image=<filename>`.
- 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}`.
Expand Down
3 changes: 3 additions & 0 deletions resources/lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/Service/ConfigurationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
82 changes: 82 additions & 0 deletions view.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

use Photobooth\Enum\FolderEnum;
use Photobooth\Service\ApplicationService;
use Photobooth\Service\LanguageService;
use Photobooth\Utility\ComponentUtility;
use Photobooth\Utility\PathUtility;

require_once __DIR__ . '/lib/boot.php';

$imageParam = $_GET['image'] ?? '';
$image = basename((string) $imageParam);

if ($image === '') {
http_response_code(400);
echo 'No image specified.';
exit();
}

$imagePath = FolderEnum::IMAGES->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');
?>
<body class="viewer-page">
<main class="viewer">
<div class="viewer__inner">
<header class="viewer__header">
<div class="viewer__title">
<?php if ($config['event']['enabled']): ?>
<span class="viewer__title-line"><?= htmlspecialchars($config['event']['textLeft']) ?></span>
<?php if (!empty($config['event']['symbol'])): ?>
<span class="viewer__title-line">
<i class="fa <?= htmlspecialchars($config['event']['symbol']) ?>" aria-hidden="true"></i>
</span>
<?php endif; ?>
<span class="viewer__title-line"><?= htmlspecialchars($config['event']['textRight']) ?></span>
<?php else: ?>
<span class="viewer__title-line"><?= htmlspecialchars(ApplicationService::getInstance()->getTitle()) ?></span>
<?php endif; ?>
</div>
</header>
<div class="viewer__accent"></div>

<div class="viewer__media" aria-label="Captured media preview">
<?php if ($isVideo): ?>
<video src="<?=$imageUrl?>" controls playsinline controlsList="nodownload">
<?=htmlspecialchars($languageService->translate('viewer_video_fallback'))?>
</video>
<?php else: ?>
<img id="viewer-image" src="<?=$imageUrl?>" alt="Captured photo">
<?php endif; ?>
</div>

<div class="viewer__actions buttonbar">
<?= ComponentUtility::renderButtonLink('download', $config['icons']['download'], $downloadUrl, true, ['download' => 'download']) ?>
</div>

</div>
</main>

<?php include PathUtility::getAbsolutePath('template/components/main.footer.php'); ?>
</body>
</html>
Loading