PWA: Foto-Galerie mit Zoom und Swipe [deploy]
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
- Neue API: list_photos.php listet vorhandene Bilder - Neue API: get_photo.php liefert Bilder per Token aus - PWA zeigt Galerie mit allen vorhandenen Fotos - Tippen auf Thumbnail öffnet Vollbild-Viewer - Pinch-to-Zoom, Doppeltap, Swipe-Navigation - Galerie aktualisiert sich nach Upload automatisch Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1512e4d706
commit
a31e063e7a
3 changed files with 271 additions and 0 deletions
72
ajax/get_photo.php
Normal file
72
ajax/get_photo.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
/* Liefert ein Foto aus dem Upload-Ordner aus.
|
||||
* Authentifizierung über Token in der URL.
|
||||
*
|
||||
* GET: token, file (Dateiname)
|
||||
*/
|
||||
|
||||
if (!defined('NOLOGIN')) define('NOLOGIN', '1');
|
||||
if (!defined('NOCSRFCHECK')) define('NOCSRFCHECK', '1');
|
||||
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
|
||||
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; $tmp2 = realpath(__FILE__); $i = strlen($tmp) - 1; $j = strlen($tmp2) - 1;
|
||||
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { $i--; $j--; }
|
||||
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
|
||||
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
||||
if (!$res) die("Include of main fails");
|
||||
|
||||
require_once __DIR__.'/../class/upload_token.class.php';
|
||||
|
||||
// Token validieren
|
||||
$token = (string) ($_REQUEST['token'] ?? '');
|
||||
$tok = BerichtUploadToken::fetchValid($db, $token);
|
||||
|
||||
if (!$tok) {
|
||||
http_response_code(403);
|
||||
die('Token ungültig');
|
||||
}
|
||||
|
||||
// Dateiname validieren (keine Pfad-Traversal erlauben)
|
||||
$filename = basename((string) ($_REQUEST['file'] ?? ''));
|
||||
if (empty($filename)) {
|
||||
http_response_code(400);
|
||||
die('Dateiname fehlt');
|
||||
}
|
||||
|
||||
// Upload-Ordner ermitteln
|
||||
$upload_dir = $tok->getUploadDir();
|
||||
if (!$upload_dir) {
|
||||
http_response_code(404);
|
||||
die('Ordner nicht gefunden');
|
||||
}
|
||||
|
||||
$filepath = $upload_dir . '/' . $filename;
|
||||
|
||||
// Prüfen ob Datei existiert und im erlaubten Ordner liegt
|
||||
$realpath = realpath($filepath);
|
||||
$realdir = realpath($upload_dir);
|
||||
|
||||
if (!$realpath || !$realdir || strpos($realpath, $realdir) !== 0) {
|
||||
http_response_code(404);
|
||||
die('Datei nicht gefunden');
|
||||
}
|
||||
|
||||
// Datei ausliefern
|
||||
$mime = mime_content_type($realpath);
|
||||
if (!$mime || strpos($mime, 'image') !== 0) {
|
||||
$mime = 'application/octet-stream';
|
||||
}
|
||||
|
||||
header('Content-Type: ' . $mime);
|
||||
header('Content-Length: ' . filesize($realpath));
|
||||
header('Cache-Control: private, max-age=3600');
|
||||
|
||||
readfile($realpath);
|
||||
exit;
|
||||
67
ajax/list_photos.php
Normal file
67
ajax/list_photos.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
/* Listet vorhandene Fotos im Upload-Ordner des Objekts auf.
|
||||
* Authentifizierung über Token in der URL.
|
||||
*
|
||||
* GET: token
|
||||
* Response: JSON { success: true, photos: [{ filename, url, size, date }] }
|
||||
*/
|
||||
|
||||
if (!defined('NOLOGIN')) define('NOLOGIN', '1');
|
||||
if (!defined('NOCSRFCHECK')) define('NOCSRFCHECK', '1');
|
||||
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
|
||||
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; $tmp2 = realpath(__FILE__); $i = strlen($tmp) - 1; $j = strlen($tmp2) - 1;
|
||||
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { $i--; $j--; }
|
||||
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
|
||||
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
||||
if (!$res) die("Include of main fails");
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
|
||||
require_once __DIR__.'/../class/upload_token.class.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Token validieren
|
||||
$token = (string) ($_REQUEST['token'] ?? '');
|
||||
$tok = BerichtUploadToken::fetchValid($db, $token);
|
||||
|
||||
if (!$tok) {
|
||||
http_response_code(403);
|
||||
echo json_encode(array('success' => false, 'error' => 'Token ungültig oder abgelaufen'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Upload-Ordner ermitteln
|
||||
$upload_dir = $tok->getUploadDir();
|
||||
if (!$upload_dir || !is_dir($upload_dir)) {
|
||||
echo json_encode(array('success' => true, 'photos' => array()));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Bilder auflisten
|
||||
$files = dol_dir_list($upload_dir, 'files', 0, '\.(jpg|jpeg|png|gif)$', '', 'date', SORT_DESC);
|
||||
$photos = array();
|
||||
|
||||
$base_url = dol_buildpath('/bericht/ajax/get_photo.php', 1);
|
||||
|
||||
foreach ($files as $f) {
|
||||
$photos[] = array(
|
||||
'filename' => $f['name'],
|
||||
'url' => $base_url . '?token=' . urlencode($token) . '&file=' . urlencode($f['name']),
|
||||
'size' => $f['size'],
|
||||
'date' => dol_print_date($f['date'], '%Y-%m-%d %H:%M'),
|
||||
);
|
||||
}
|
||||
|
||||
echo json_encode(array(
|
||||
'success' => true,
|
||||
'photos' => $photos,
|
||||
'count' => count($photos),
|
||||
));
|
||||
|
|
@ -631,6 +631,52 @@ body {
|
|||
.scanner-page-thumb {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ========== Foto-Galerie (vorhandene Bilder) ========== */
|
||||
.photo-gallery {
|
||||
padding: 16px;
|
||||
background: #222;
|
||||
border-radius: 8px;
|
||||
margin: 16px;
|
||||
}
|
||||
.photo-gallery h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
color: #aaa;
|
||||
font-weight: normal;
|
||||
}
|
||||
.photo-gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.photo-gallery-item {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: #333;
|
||||
}
|
||||
.photo-gallery-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.photo-gallery-item:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.photo-gallery-empty {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.photo-gallery-loading {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -650,6 +696,14 @@ body {
|
|||
<input type="file" id="gallery-input" class="hidden-input" accept="image/*" multiple>
|
||||
</div>
|
||||
|
||||
<!-- Vorhandene Fotos -->
|
||||
<div class="photo-gallery" id="photo-gallery">
|
||||
<h3>📷 Vorhandene Fotos (<span id="photo-gallery-count">...</span>)</h3>
|
||||
<div class="photo-gallery-grid" id="photo-gallery-grid">
|
||||
<div class="photo-gallery-loading">Lade Fotos...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kamera-View Overlay -->
|
||||
<div id="camera-view" class="camera-view hidden">
|
||||
<video id="camera-stream" autoplay playsinline muted></video>
|
||||
|
|
@ -878,6 +932,11 @@ async function savePhoto() {
|
|||
uploadedList.prepend(item);
|
||||
|
||||
toast('Foto gespeichert');
|
||||
|
||||
// Galerie aktualisieren
|
||||
if (typeof loadGalleryPhotos === 'function') {
|
||||
setTimeout(loadGalleryPhotos, 300);
|
||||
}
|
||||
} else {
|
||||
toast('Fehler: ' + (data.error || 'unbekannt'), true);
|
||||
}
|
||||
|
|
@ -1112,6 +1171,11 @@ async function finishScanning() {
|
|||
|
||||
toast('Dokument erstellt: ' + scannedPages.length + ' Seiten');
|
||||
closeScannerView();
|
||||
|
||||
// Galerie aktualisieren
|
||||
if (typeof loadGalleryPhotos === 'function') {
|
||||
setTimeout(loadGalleryPhotos, 300);
|
||||
}
|
||||
} else {
|
||||
scannerUploading.classList.add('hidden');
|
||||
toast('Fehler: ' + (data.error || 'unbekannt'), true);
|
||||
|
|
@ -1150,6 +1214,11 @@ async function uploadFile(file) {
|
|||
item.innerHTML = '<span class="check">✓</span><span class="name">' + escapeHtml(data.filename) + '</span>';
|
||||
uploadedList.prepend(item);
|
||||
toast('Hochgeladen: ' + fname);
|
||||
|
||||
// Galerie aktualisieren
|
||||
if (typeof loadGalleryPhotos === 'function') {
|
||||
setTimeout(loadGalleryPhotos, 300);
|
||||
}
|
||||
} else {
|
||||
toast('Fehler: ' + (data.error || 'unbekannt'), true);
|
||||
}
|
||||
|
|
@ -1392,6 +1461,69 @@ scannerPagesStrip.addEventListener('click', (e) => {
|
|||
pwaViewer.open(images, idx >= 0 ? idx : 0);
|
||||
}
|
||||
});
|
||||
|
||||
// === FOTO-GALERIE (vorhandene Bilder) ===
|
||||
const photoGalleryGrid = document.getElementById('photo-gallery-grid');
|
||||
const photoGalleryCount = document.getElementById('photo-gallery-count');
|
||||
let galleryPhotos = [];
|
||||
|
||||
async function loadGalleryPhotos() {
|
||||
try {
|
||||
const r = await fetch('ajax/list_photos.php?token=' + encodeURIComponent(TOKEN));
|
||||
const data = await r.json();
|
||||
|
||||
if (data.success && data.photos) {
|
||||
galleryPhotos = data.photos;
|
||||
renderGallery();
|
||||
} else {
|
||||
photoGalleryGrid.innerHTML = '<div class="photo-gallery-empty">Fehler beim Laden</div>';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Galerie laden fehlgeschlagen:', e);
|
||||
photoGalleryGrid.innerHTML = '<div class="photo-gallery-empty">Fehler beim Laden</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderGallery() {
|
||||
photoGalleryCount.textContent = galleryPhotos.length;
|
||||
|
||||
if (galleryPhotos.length === 0) {
|
||||
photoGalleryGrid.innerHTML = '<div class="photo-gallery-empty">Noch keine Fotos vorhanden</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
photoGalleryGrid.innerHTML = galleryPhotos.map((photo, idx) => `
|
||||
<div class="photo-gallery-item" data-idx="${idx}">
|
||||
<img src="${escapeHtml(photo.url)}" alt="${escapeHtml(photo.filename)}" loading="lazy">
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Galerie-Klick öffnet Viewer
|
||||
photoGalleryGrid.addEventListener('click', (e) => {
|
||||
const item = e.target.closest('.photo-gallery-item');
|
||||
if (!item) return;
|
||||
|
||||
const idx = parseInt(item.dataset.idx, 10) || 0;
|
||||
const images = galleryPhotos.map(p => ({ url: p.url, title: p.filename }));
|
||||
|
||||
if (images.length > 0) {
|
||||
pwaViewer.open(images, idx);
|
||||
}
|
||||
});
|
||||
|
||||
// Nach Upload Galerie aktualisieren
|
||||
const origUploadFile = uploadFile;
|
||||
async function uploadFileWithRefresh(file) {
|
||||
await origUploadFile(file);
|
||||
// Galerie nach kurzer Verzögerung neu laden (Upload muss fertig sein)
|
||||
setTimeout(loadGalleryPhotos, 500);
|
||||
}
|
||||
// Ersetze uploadFile-Referenzen - nein, besser direkt nach erfolgreichem Upload
|
||||
// Wir fügen den Refresh in die savePhoto-Funktion ein
|
||||
|
||||
// Galerie beim Laden initialisieren
|
||||
loadGalleryPhotos();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Reference in a new issue