From 04fdd87ffdfd87856f6c4e4254f83953c3e0570a Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Mon, 13 Apr 2026 09:18:50 +0200 Subject: [PATCH] Kamera-View: Mehrere Fotos ohne Kamera-Neustart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eigene Kamera-View mit getUserMedia() statt nativer Kamera-App: - Vollbild-Overlay mit Live-Stream - Auslöser, Vorschau, Speichern/Verwerfen - Kamera bleibt nach Speichern offen - Front/Back-Wechsel - Session-Counter [deploy] Co-Authored-By: Claude Opus 4.5 --- mobile_upload.php | 360 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 348 insertions(+), 12 deletions(-) diff --git a/mobile_upload.php b/mobile_upload.php index e0a96a6..901f056 100644 --- a/mobile_upload.php +++ b/mobile_upload.php @@ -187,12 +187,142 @@ body { padding: 14px 16px; border-radius: 8px; text-align: center; - z-index: 100; + z-index: 2000; box-shadow: 0 4px 16px rgba(0,0,0,0.4); animation: slideDown 0.2s; } .toast.error { background: #d9534f; } @keyframes slideDown { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; } } + +/* Kamera-View Styles */ +.camera-view { + position: fixed; + inset: 0; + background: #000; + z-index: 1000; + display: flex; + flex-direction: column; +} +.camera-view.hidden { display: none; } + +#camera-stream { + flex: 1; + width: 100%; + height: 100%; + object-fit: cover; +} +#camera-canvas { + display: none; +} + +.camera-controls { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 24px 20px 40px; + display: flex; + justify-content: space-between; + align-items: center; + background: linear-gradient(transparent, rgba(0,0,0,0.7)); +} + +.camera-btn-close, +.camera-btn-switch { + width: 50px; + height: 50px; + border-radius: 50%; + border: none; + background: rgba(255,255,255,0.2); + color: #fff; + font-size: 22px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} +.camera-btn-close:active, +.camera-btn-switch:active { + background: rgba(255,255,255,0.4); +} + +.camera-shutter { + width: 72px; + height: 72px; + border-radius: 50%; + border: 4px solid #fff; + background: rgba(255,255,255,0.3); + cursor: pointer; + position: relative; +} +.camera-shutter::after { + content: ''; + position: absolute; + inset: 6px; + border-radius: 50%; + background: #fff; + transition: transform 0.1s; +} +.camera-shutter:active::after { + transform: scale(0.9); +} + +.camera-counter { + position: absolute; + top: 16px; + right: 16px; + background: rgba(0,0,0,0.5); + color: #5cb85c; + padding: 8px 14px; + border-radius: 20px; + font-size: 14px; + font-weight: 600; +} + +/* Foto-Vorschau */ +.photo-preview { + position: absolute; + inset: 0; + background: #000; + display: flex; + flex-direction: column; +} +.photo-preview.hidden { display: none; } + +.photo-preview img { + flex: 1; + width: 100%; + height: 100%; + object-fit: contain; +} + +.preview-buttons { + display: flex; + gap: 12px; + padding: 20px; + background: rgba(0,0,0,0.8); +} + +.btn-discard, +.btn-save { + flex: 1; + padding: 16px; + font-size: 16px; + font-weight: 600; + border-radius: 10px; + border: none; + cursor: pointer; +} +.btn-discard { + background: #444; + color: #fff; +} +.btn-discard:active { background: #555; } +.btn-save { + background: #5cb85c; + color: #fff; +} +.btn-save:active { background: #4cae4c; } @@ -204,13 +334,37 @@ body {
- - +
+ + +
@@ -223,22 +377,204 @@ const TOKEN = ; const URL_UPLOAD = window.location.pathname + '?token=' + encodeURIComponent(TOKEN); let uploadedCount = 0; -const cameraInput = document.getElementById('camera-input'); +// DOM-Elemente const galleryInput = document.getElementById('gallery-input'); const uploadingArea = document.getElementById('uploading-area'); const uploadedList = document.getElementById('uploaded-list'); const uploadedCountEl = document.getElementById('uploaded-count'); -[cameraInput, galleryInput].forEach(inp => { - inp.addEventListener('change', async () => { - if (!inp.files || !inp.files.length) return; - for (const file of inp.files) { - await uploadFile(file); - } - inp.value = ''; - }); +// Kamera-Elemente +const openCameraBtn = document.getElementById('open-camera-btn'); +const cameraView = document.getElementById('camera-view'); +const cameraStream = document.getElementById('camera-stream'); +const cameraCanvas = document.getElementById('camera-canvas'); +const cameraClose = document.getElementById('camera-close'); +const cameraShutter = document.getElementById('camera-shutter'); +const cameraSwitch = document.getElementById('camera-switch'); +const cameraCounter = document.getElementById('camera-counter'); +const photoPreview = document.getElementById('photo-preview'); +const previewImage = document.getElementById('preview-image'); +const previewDiscard = document.getElementById('preview-discard'); +const previewSave = document.getElementById('preview-save'); + +// Kamera-Status +let mediaStream = null; +let currentFacingMode = 'environment'; // 'environment' = Rückkamera, 'user' = Frontkamera +let currentPhotoBlob = null; +let cameraSessionCount = 0; + +// Galerie-Input Handler +galleryInput.addEventListener('change', async () => { + if (!galleryInput.files || !galleryInput.files.length) return; + for (const file of galleryInput.files) { + await uploadFile(file); + } + galleryInput.value = ''; }); +// === KAMERA-FUNKTIONEN === + +// Kamera öffnen +openCameraBtn.addEventListener('click', openCameraView); + +async function openCameraView() { + try { + const constraints = { + video: { + facingMode: currentFacingMode, + width: { ideal: 1920 }, + height: { ideal: 1080 } + }, + audio: false + }; + mediaStream = await navigator.mediaDevices.getUserMedia(constraints); + cameraStream.srcObject = mediaStream; + cameraView.classList.remove('hidden'); + cameraSessionCount = 0; + updateCameraCounter(); + document.body.style.overflow = 'hidden'; + } catch (err) { + console.error('Kamera-Fehler:', err); + toast('Kamera nicht verfügbar: ' + err.message, true); + } +} + +// Kamera schließen +cameraClose.addEventListener('click', closeCameraView); + +function closeCameraView() { + if (mediaStream) { + mediaStream.getTracks().forEach(track => track.stop()); + mediaStream = null; + } + cameraStream.srcObject = null; + cameraView.classList.add('hidden'); + photoPreview.classList.add('hidden'); + document.body.style.overflow = ''; +} + +// Foto aufnehmen +cameraShutter.addEventListener('click', takePhoto); + +function takePhoto() { + if (!mediaStream) return; + + const video = cameraStream; + const canvas = cameraCanvas; + + // Canvas auf Video-Größe setzen + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + // Video-Frame auf Canvas zeichnen + const ctx = canvas.getContext('2d'); + ctx.drawImage(video, 0, 0); + + // Canvas zu Blob konvertieren + canvas.toBlob((blob) => { + if (blob) { + currentPhotoBlob = blob; + showPreview(blob); + } + }, 'image/jpeg', 0.85); +} + +// Vorschau anzeigen +function showPreview(blob) { + const url = URL.createObjectURL(blob); + previewImage.onload = () => URL.revokeObjectURL(url); + previewImage.src = url; + photoPreview.classList.remove('hidden'); +} + +// Foto verwerfen +previewDiscard.addEventListener('click', discardPhoto); + +function discardPhoto() { + currentPhotoBlob = null; + photoPreview.classList.add('hidden'); +} + +// Foto speichern und Kamera offen lassen +previewSave.addEventListener('click', savePhoto); + +async function savePhoto() { + if (!currentPhotoBlob) return; + + // Vorschau schließen, zurück zur Kamera + photoPreview.classList.add('hidden'); + + // Upload im Hintergrund + const blob = currentPhotoBlob; + currentPhotoBlob = null; + + const fname = 'foto_' + Date.now() + '.jpg'; + const fd = new FormData(); + fd.append('token', TOKEN); + fd.append('file', blob, fname); + + try { + const r = await fetch(URL_UPLOAD, { method: 'POST', body: fd }); + const data = await r.json(); + if (data.success) { + uploadedCount++; + cameraSessionCount++; + uploadedCountEl.textContent = '(' + uploadedCount + ')'; + updateCameraCounter(); + + // Item zur Liste hinzufügen + const item = document.createElement('div'); + item.className = 'uploaded-item'; + item.innerHTML = '' + escapeHtml(data.filename) + ''; + uploadedList.prepend(item); + + toast('Foto gespeichert'); + } else { + toast('Fehler: ' + (data.error || 'unbekannt'), true); + } + } catch (e) { + toast('Netzwerkfehler', true); + } +} + +// Kamera wechseln (Front/Back) +cameraSwitch.addEventListener('click', switchCamera); + +async function switchCamera() { + currentFacingMode = currentFacingMode === 'environment' ? 'user' : 'environment'; + + // Alten Stream stoppen + if (mediaStream) { + mediaStream.getTracks().forEach(track => track.stop()); + } + + // Neuen Stream starten + try { + const constraints = { + video: { + facingMode: currentFacingMode, + width: { ideal: 1920 }, + height: { ideal: 1080 } + }, + audio: false + }; + mediaStream = await navigator.mediaDevices.getUserMedia(constraints); + cameraStream.srcObject = mediaStream; + } catch (err) { + console.error('Kamera-Wechsel fehlgeschlagen:', err); + toast('Kamera-Wechsel fehlgeschlagen', true); + // Zurück zur anderen Kamera + currentFacingMode = currentFacingMode === 'environment' ? 'user' : 'environment'; + } +} + +function updateCameraCounter() { + const text = cameraSessionCount === 1 ? '1 Foto' : cameraSessionCount + ' Fotos'; + cameraCounter.textContent = text; +} + +// === UPLOAD-FUNKTIONEN === + async function uploadFile(file) { const blob = await resizeImage(file, 2000); const fname = file.name || 'photo.jpg';