Skip to content

Commit 8741edf

Browse files
authored
Feature: add view image page (#1389)
* add a view page for qr code link
1 parent af38e9c commit 8741edf

File tree

7 files changed

+249
-1
lines changed

7 files changed

+249
-1
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Standalone viewer page (view.php)
2+
3+
:root {
4+
// mirror gallery style tokens
5+
--viewer-background: var(--primary-light-color);
6+
--viewer-foreground: var(--font-color, #111);
7+
--viewer-surface: #ffffff;
8+
--viewer-accent: var(--secondary-color, #444);
9+
--viewer-accent-foreground: var(--secondary-font-color, #fff);
10+
--viewer-shadow: 0 18px 48px rgba(0, 0, 0, 0.25);
11+
}
12+
13+
.viewer-page {
14+
margin: 0;
15+
min-height: 100vh;
16+
background:
17+
radial-gradient(circle at 20% 20%, color-mix(in srgb, var(--viewer-background), white 10%), transparent 32%),
18+
radial-gradient(circle at 85% 10%, color-mix(in srgb, var(--primary-color, #2196f3), white 12%), transparent 28%),
19+
linear-gradient(
20+
135deg,
21+
color-mix(in srgb, var(--viewer-background), var(--primary-color, #2196f3) 35%),
22+
var(--primary-color, #2196f3)
23+
);
24+
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
25+
color: var(--viewer-foreground);
26+
display: flex;
27+
align-items: center;
28+
justify-content: center;
29+
padding: 24px;
30+
}
31+
32+
.viewer {
33+
width: min(960px, 100%);
34+
background: rgba(255, 255, 255, 0.12);
35+
backdrop-filter: blur(12px);
36+
border-radius: 18px;
37+
box-shadow: var(--viewer-shadow);
38+
padding: clamp(16px, 3vw, 24px);
39+
border: 1px solid rgba(255, 255, 255, 0.12);
40+
}
41+
42+
.viewer__inner {
43+
background: var(--viewer-surface);
44+
border-radius: 14px;
45+
padding: clamp(14px, 3vw, 22px);
46+
display: flex;
47+
flex-direction: column;
48+
gap: 16px;
49+
border: 1px solid rgba(0, 0, 0, 0.05);
50+
}
51+
52+
.viewer__header {
53+
display: flex;
54+
flex-direction: column;
55+
align-items: center;
56+
gap: 10px;
57+
}
58+
59+
.viewer__title {
60+
margin: 0;
61+
width: 100%;
62+
text-align: center;
63+
display: flex;
64+
flex-direction: column;
65+
gap: 0.2em;
66+
}
67+
68+
.viewer__title-line {
69+
font-size: clamp(22px, 5vw, 34px);
70+
color: var(--viewer-accent);
71+
line-height: 1.05;
72+
font-weight: 700;
73+
letter-spacing: 0.01em;
74+
}
75+
76+
.viewer__accent {
77+
height: 6px;
78+
width: 100%;
79+
border-radius: 999px;
80+
background: linear-gradient(90deg, var(--primary-color, #2196f3), var(--secondary-color, #3f51b5));
81+
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
82+
}
83+
84+
.viewer__badge {
85+
background: var(--viewer-accent);
86+
color: var(--viewer-accent-foreground);
87+
padding: 6px 10px;
88+
border-radius: 999px;
89+
font-size: 12px;
90+
letter-spacing: 0.04em;
91+
text-transform: uppercase;
92+
}
93+
94+
.viewer__media {
95+
position: relative;
96+
border-radius: 12px;
97+
overflow: hidden;
98+
background: #f5f5f5;
99+
border: 2px solid rgba(0, 0, 0, 0.05);
100+
max-height: 70vh;
101+
display: grid;
102+
place-items: center;
103+
box-shadow: inset 0 12px 22px rgba(0, 0, 0, 0.04);
104+
}
105+
106+
.viewer__media img,
107+
.viewer__media video {
108+
display: block;
109+
width: 100%;
110+
height: auto;
111+
object-fit: contain;
112+
}
113+
114+
.viewer__media video {
115+
background: #000;
116+
}
117+
118+
.viewer__btn {
119+
min-height: 56px;
120+
touch-action: manipulation;
121+
user-select: none;
122+
-webkit-tap-highlight-color: transparent;
123+
padding-inline: 2.2rem;
124+
width: 100%;
125+
}
126+
127+
.viewer__tip {
128+
margin: 0;
129+
font-size: 14px;
130+
color: rgba(0, 0, 0, 0.64);
131+
text-align: center;
132+
}
133+
134+
@media (min-width: 720px) {
135+
.viewer__actions {
136+
grid-template-columns: repeat(2, 1fr);
137+
}
138+
}
139+
140+
@media (max-width: 540px) {
141+
.viewer {
142+
padding: 12px;
143+
}
144+
145+
.viewer__inner {
146+
padding: 14px;
147+
}
148+
149+
.viewer__title {
150+
font-size: clamp(18px, 6vw, 26px);
151+
}
152+
}

assets/sass/framework.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
@use 'components/virtualKeyboard';
2424
@use 'components/github-corner';
2525
@use 'components/background';
26+
@use 'components/viewer';
2627

2728
// Experiments
2829
@use 'experiments/video-capture-animation';

docs/faq/index.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,13 @@ sudo -u www-data scp /var/www/html/data/images/20230129_125148.jpg [username@rem
479479
480480
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.
481481
482+
## How do I use QR codes for downloads?
483+
484+
- Touch-friendly viewer page: `view.php?image=<filename>` shows the photo/video with a large download button.
485+
- Direct file download (no UI): `api/download.php?image=<filename>`.
486+
- 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.
487+
- 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.
488+
482489
## How to use the image randomizer
483490
484491
To use the image randomizer images must be placed inside `private/images/{folderName}`.

resources/lang/de.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,9 @@
853853
"qr:qr_text": "Eigener Hilfetext",
854854
"qr:qr_url": "URL für QR-Code",
855855
"qrHelp": "Um das Bild auf Ihr Handy herunterzuladen, verbinden Sie sich mit dem WLAN:",
856+
"share": "Teilen",
857+
"viewer_photo_title": "Dein Foto",
858+
"viewer_video_fallback": "Dein Browser kann dieses Video nicht abspielen.",
856859
"really_delete": "Wirklich nach Ihren Einstellungen zurücksetzen? Dies kann nicht rückgängig gemacht werden!",
857860
"really_delete_image": "wird gelöscht! Dies kann nicht rückgängig gemacht werden! Bild wirklich löschen?",
858861
"reboot_button": "Neustart",

resources/lang/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,9 @@
855855
"qr:qr_text": "Own help text",
856856
"qr:qr_url": "URL for QR Code",
857857
"qrHelp": "To download the picture to your smartphone, connect to the WiFi:",
858+
"share": "Share",
859+
"viewer_photo_title": "Your photo",
860+
"viewer_video_fallback": "Your browser can’t play this video.",
858861
"really_delete": "Really reset according to your settings? This cannot be undone!",
859862
"really_delete_image": "will be deleted! This cannot be undone! Really delete picture?",
860863
"reboot_button": "Reboot",

src/Service/ConfigurationService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ protected function addDefaults(array $config): array
120120
}
121121

122122
if (empty($config['qr']['url'])) {
123-
$config['qr']['url'] = 'api/download.php?image=';
123+
$config['qr']['url'] = 'view.php?image=';
124124
}
125125

126126
return $config;

view.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
use Photobooth\Enum\FolderEnum;
4+
use Photobooth\Service\ApplicationService;
5+
use Photobooth\Service\LanguageService;
6+
use Photobooth\Utility\ComponentUtility;
7+
use Photobooth\Utility\PathUtility;
8+
9+
require_once __DIR__ . '/lib/boot.php';
10+
11+
$imageParam = $_GET['image'] ?? '';
12+
$image = basename((string) $imageParam);
13+
14+
if ($image === '') {
15+
http_response_code(400);
16+
echo 'No image specified.';
17+
exit();
18+
}
19+
20+
$imagePath = FolderEnum::IMAGES->absolute() . DIRECTORY_SEPARATOR . $image;
21+
if (!is_file($imagePath)) {
22+
http_response_code(404);
23+
echo 'Image not found.';
24+
exit();
25+
}
26+
27+
$extension = strtolower(pathinfo($imagePath, PATHINFO_EXTENSION));
28+
$isVideo = in_array($extension, ['mp4', 'mov', 'webm'], true);
29+
$mime = match ($extension) {
30+
'png' => 'image/png',
31+
'gif' => 'image/gif',
32+
default => 'image/jpeg',
33+
};
34+
$imageUrl = PathUtility::getPublicPath(FolderEnum::IMAGES->value . '/' . rawurlencode($image));
35+
$downloadUrl = PathUtility::getPublicPath('api/download.php?image=' . rawurlencode($image));
36+
$languageService = LanguageService::getInstance();
37+
$pageTitle = ApplicationService::getInstance()->getTitle() . ' - ' . $languageService->translate('viewer_photo_title');
38+
$photoswipe = false;
39+
$remoteBuzzer = false;
40+
41+
include PathUtility::getAbsolutePath('template/components/main.head.php');
42+
?>
43+
<body class="viewer-page">
44+
<main class="viewer">
45+
<div class="viewer__inner">
46+
<header class="viewer__header">
47+
<div class="viewer__title">
48+
<?php if ($config['event']['enabled']): ?>
49+
<span class="viewer__title-line"><?= htmlspecialchars($config['event']['textLeft']) ?></span>
50+
<?php if (!empty($config['event']['symbol'])): ?>
51+
<span class="viewer__title-line">
52+
<i class="fa <?= htmlspecialchars($config['event']['symbol']) ?>" aria-hidden="true"></i>
53+
</span>
54+
<?php endif; ?>
55+
<span class="viewer__title-line"><?= htmlspecialchars($config['event']['textRight']) ?></span>
56+
<?php else: ?>
57+
<span class="viewer__title-line"><?= htmlspecialchars(ApplicationService::getInstance()->getTitle()) ?></span>
58+
<?php endif; ?>
59+
</div>
60+
</header>
61+
<div class="viewer__accent"></div>
62+
63+
<div class="viewer__media" aria-label="Captured media preview">
64+
<?php if ($isVideo): ?>
65+
<video src="<?=$imageUrl?>" controls playsinline controlsList="nodownload">
66+
<?=htmlspecialchars($languageService->translate('viewer_video_fallback'))?>
67+
</video>
68+
<?php else: ?>
69+
<img id="viewer-image" src="<?=$imageUrl?>" alt="Captured photo">
70+
<?php endif; ?>
71+
</div>
72+
73+
<div class="viewer__actions buttonbar">
74+
<?= ComponentUtility::renderButtonLink('download', $config['icons']['download'], $downloadUrl, true, ['download' => 'download']) ?>
75+
</div>
76+
77+
</div>
78+
</main>
79+
80+
<?php include PathUtility::getAbsolutePath('template/components/main.footer.php'); ?>
81+
</body>
82+
</html>

0 commit comments

Comments
 (0)