PWA: PDF-Viewer mit PDF.js (inline, kein Download nötig) [deploy]
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 5s
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 5s
This commit is contained in:
parent
a192cbfa27
commit
125a6302ab
5 changed files with 139 additions and 2 deletions
26
app.css
26
app.css
|
|
@ -589,7 +589,7 @@ body {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Doc-Viewer (PDF/Image inline) */
|
/* Doc-Viewer (Bilder) */
|
||||||
.doc-viewer-modal .dv-body {
|
.doc-viewer-modal .dv-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -605,6 +605,30 @@ body {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* PDF-Viewer */
|
||||||
|
.pdf-viewer-modal .fs-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.pdf-viewer-modal .fs-title { order: 4; }
|
||||||
|
.pdf-viewer-modal .icon-btn:nth-of-type(1) { order: 1; }
|
||||||
|
.pdf-viewer-modal .pdf-page-info { order: 2; font-size: 12px; color: #aaa; }
|
||||||
|
.pdf-viewer-modal .icon-btn:nth-of-type(2) { order: 3; }
|
||||||
|
.pdf-viewer-modal .icon-btn:nth-of-type(3) { order: 5; }
|
||||||
|
.pdf-viewer-modal .fs-header a { order: 6; }
|
||||||
|
.pdf-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f5f5f5;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.pdf-body canvas { max-width: 100%; height: auto; box-shadow: 0 2px 8px rgba(0,0,0,0.2); }
|
||||||
|
|
||||||
/* PIN-Modal */
|
/* PIN-Modal */
|
||||||
.pin-modal .pin-body {
|
.pin-modal .pin-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
||||||
62
app.js
62
app.js
|
|
@ -1794,9 +1794,15 @@ function formatShortDate(ts) {
|
||||||
|
|
||||||
function openFileViewer({ url, blob, mime }, filename) {
|
function openFileViewer({ url, blob, mime }, filename) {
|
||||||
const isImage = (mime || '').startsWith('image/');
|
const isImage = (mime || '').startsWith('image/');
|
||||||
|
const isPdf = (mime || '').includes('pdf') || /\.pdf$/i.test(filename);
|
||||||
|
|
||||||
|
if (isPdf && typeof pdfjs !== 'undefined') {
|
||||||
|
openPdfViewer(blob, filename, url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isImage) {
|
if (!isImage) {
|
||||||
// Alles außer Bildern: direkt Download
|
// Alles außer Bildern + PDFs: Download
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = filename || 'download';
|
a.download = filename || 'download';
|
||||||
|
|
@ -1827,6 +1833,60 @@ function openFileViewer({ url, blob, mime }, filename) {
|
||||||
modal.querySelector('#dv-close').onclick = cleanup;
|
modal.querySelector('#dv-close').onclick = cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openPdfViewer(blob, filename, blobUrl) {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'fullscreen-modal pdf-viewer-modal';
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="fs-header">
|
||||||
|
<button class="icon-btn" id="pdf-prev" title="← Seite zurück">‹</button>
|
||||||
|
<div class="pdf-page-info" id="pdf-page-info">⏳</div>
|
||||||
|
<button class="icon-btn" id="pdf-next" title="Seite vor →">›</button>
|
||||||
|
<div class="fs-title">${escapeHtml(filename || '')}</div>
|
||||||
|
<button class="icon-btn" id="pdf-close">✕</button>
|
||||||
|
<a class="icon-btn" id="pdf-download" title="Download" download="${escapeHtml(filename || 'download')}" href="${blobUrl}">⬇</a>
|
||||||
|
</div>
|
||||||
|
<div class="pdf-body" id="pdf-body">
|
||||||
|
<canvas id="pdf-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
const canvas = modal.querySelector('#pdf-canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const pageInfo = modal.querySelector('#pdf-page-info');
|
||||||
|
let pdf, currentPage = 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
pdf = await pdfjs.getDocument(new Uint8Array(await blob.arrayBuffer())).promise;
|
||||||
|
pageInfo.textContent = `1 / ${pdf.numPages}`;
|
||||||
|
|
||||||
|
async function renderPage(num) {
|
||||||
|
if (num < 1 || num > pdf.numPages) return;
|
||||||
|
currentPage = num;
|
||||||
|
const page = await pdf.getPage(num);
|
||||||
|
const viewport = page.getViewport({ scale: window.devicePixelRatio });
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||||
|
pageInfo.textContent = `${num} / ${pdf.numPages}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await renderPage(1);
|
||||||
|
modal.querySelector('#pdf-prev').onclick = () => renderPage(currentPage - 1);
|
||||||
|
modal.querySelector('#pdf-next').onclick = () => renderPage(currentPage + 1);
|
||||||
|
modal.querySelector('#pdf-prev').disabled = false;
|
||||||
|
modal.querySelector('#pdf-next').disabled = pdf.numPages === 1;
|
||||||
|
} catch (err) {
|
||||||
|
pageInfo.textContent = '❌ PDF-Fehler';
|
||||||
|
console.error('PDF load error:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.querySelector('#pdf-close').onclick = () => {
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
modal.remove();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
* PHOTO VOLLBILD MODAL MIT ZOOM + SWIPE + SKIZZEN-EDITOR
|
* PHOTO VOLLBILD MODAL MIT ZOOM + SWIPE + SKIZZEN-EDITOR
|
||||||
* ============================================================ */
|
* ============================================================ */
|
||||||
|
|
|
||||||
|
|
@ -62,12 +62,21 @@ header('Expires: 0');
|
||||||
|
|
||||||
<div id="toast-container"></div>
|
<div id="toast-container"></div>
|
||||||
|
|
||||||
|
<script src="lib/pdf.min.js?v=<?php echo $jsVersion; ?>"></script>
|
||||||
<script src="lib/idb.js?v=<?php echo $jsVersion; ?>"></script>
|
<script src="lib/idb.js?v=<?php echo $jsVersion; ?>"></script>
|
||||||
<script src="lib/api.js?v=<?php echo $jsVersion; ?>"></script>
|
<script src="lib/api.js?v=<?php echo $jsVersion; ?>"></script>
|
||||||
<script src="lib/offline.js?v=<?php echo $jsVersion; ?>"></script>
|
<script src="lib/offline.js?v=<?php echo $jsVersion; ?>"></script>
|
||||||
<script src="lib/router.js?v=<?php echo $jsVersion; ?>"></script>
|
<script src="lib/router.js?v=<?php echo $jsVersion; ?>"></script>
|
||||||
<script src="app.js?v=<?php echo $jsVersion; ?>"></script>
|
<script src="app.js?v=<?php echo $jsVersion; ?>"></script>
|
||||||
<script>
|
<script>
|
||||||
|
if (typeof pdfjsWorker === 'undefined') {
|
||||||
|
// Setze PDF.js Worker-Pfad
|
||||||
|
if (typeof pdfjs !== 'undefined') {
|
||||||
|
pdfjs.GlobalWorkerOptions.workerSrc = 'lib/pdf.worker.min.js?v=<?php echo $jsVersion; ?>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
/* PWA Install Prompt abfangen */
|
/* PWA Install Prompt abfangen */
|
||||||
let deferredInstallPrompt = null;
|
let deferredInstallPrompt = null;
|
||||||
window.addEventListener('beforeinstallprompt', function (e) {
|
window.addEventListener('beforeinstallprompt', function (e) {
|
||||||
|
|
|
||||||
22
lib/pdf.min.js
vendored
Normal file
22
lib/pdf.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
22
lib/pdf.worker.min.js
vendored
Normal file
22
lib/pdf.worker.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue