Compare commits

...

3 Commits

3 changed files with 81 additions and 24 deletions

View File

@ -274,7 +274,7 @@ return [
*/
'payload' => [
'max_size' => 1024 * 1024, // 1MB - maximum request payload size in bytes
'max_size' => 1024 * 1024 * 20, // 1MB - maximum request payload size in bytes
'max_nesting_depth' => 10, // Maximum depth of dot-notation property paths
'max_calls' => 50, // Maximum method calls per request
'max_components' => 200, // Maximum components per batch request

View File

@ -9,13 +9,11 @@
@endif
@livewireStyles
</head>
<body class="bg-gray-950 text-white h-screen flex flex-col items-center p-6 overflow-hidden">
<body class="bg-gray-950 text-white min-h-screen flex flex-col items-center justify-center p-6">
<h1 class="shrink-0 text-3xl font-bold mb-8 tracking-wide">Fotobox</h1>
<h1 class="text-3xl font-bold mb-8 tracking-wide">Fotobox</h1>
<div class="flex-1 min-h-0 w-full flex flex-col items-center">
@livewire('camera-capture')
</div>
@livewire('camera-capture')
@livewireScripts
</body>

View File

@ -1,23 +1,21 @@
<div class="flex flex-col items-center w-full h-full"
<div class="flex flex-col items-center w-full"
x-data="{ lightbox: null }"
x-on:keydown.escape.window="lightbox = null">
<div class="flex-1 min-h-0 w-full flex justify-center">
<div class="relative h-full" style="aspect-ratio: 16/9; max-width: 100%;">
<video id="video" autoplay playsinline
class="w-full h-full rounded-2xl shadow-2xl bg-gray-900" style="transform: scaleX(-1);"></video>
<div class="relative w-full max-w-2xl">
<video id="video" autoplay playsinline
class="w-full rounded-2xl shadow-2xl bg-gray-900 aspect-video object-cover" style="transform: scaleX(-1);"></video>
<canvas id="canvas" class="hidden"></canvas>
<canvas id="canvas" class="hidden"></canvas>
<div id="flash" class="absolute inset-0 rounded-2xl bg-white opacity-0 pointer-events-none transition-opacity duration-150"></div>
<div id="flash" class="absolute inset-0 rounded-2xl bg-white opacity-0 pointer-events-none transition-opacity duration-150"></div>
<div id="countdown" class="absolute inset-0 rounded-2xl hidden items-center justify-center bg-black/40">
<span id="countdown-num" class="text-white font-bold drop-shadow-lg" style="font-size: 8rem; line-height: 1;"></span>
</div>
<div id="countdown" class="absolute inset-0 rounded-2xl hidden items-center justify-center bg-black/40">
<span id="countdown-num" class="text-white font-bold drop-shadow-lg" style="font-size: 8rem; line-height: 1;"></span>
</div>
</div>
<div class="shrink-0 mt-6 w-full max-w-xs">
<div class="mt-6 w-full max-w-xs">
<select wire:model="photoprofileId"
class="w-full rounded-full bg-black/50 text-white border border-white/20 px-5 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-white/40 appearance-none text-center">
@foreach ($photoprofiles as $id => $name)
@ -26,18 +24,23 @@
</select>
</div>
<div id="camera-select-wrap" class="mt-3 w-full max-w-xs hidden">
<select id="camera-select"
class="w-full rounded-full bg-black/50 text-white border border-white/20 px-5 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-white/40 appearance-none text-center"></select>
</div>
<button id="capture-btn"
wire:loading.attr="disabled"
@disabled($hasProcessing)
class="shrink-0 mt-8 px-10 py-4 bg-white text-gray-950 font-semibold text-lg rounded-full shadow-lg hover:bg-gray-200 active:scale-95 transition-all duration-150 disabled:opacity-50 disabled:cursor-not-allowed">
class="mt-8 px-10 py-4 bg-white text-gray-950 font-semibold text-lg rounded-full shadow-lg hover:bg-gray-200 active:scale-95 transition-all duration-150 disabled:opacity-50 disabled:cursor-not-allowed">
<span wire:loading.remove>{{ $hasProcessing ? 'Erstelle Bild...' : 'Foto aufnehmen' }}</span>
<span wire:loading>Saving…</span>
</button>
<div class="shrink-0 mt-4 text-sm text-gray-400 h-6">{{ $status }}</div>
<div class="mt-4 text-sm text-gray-400 h-6">{{ $status }}</div>
@if ($recentPhotos->isNotEmpty())
<div class="shrink-0 mt-10 w-full max-w-2xl">
<div class="mt-10 w-full max-w-2xl">
<h2 class="text-sm font-medium text-gray-400 mb-3 tracking-wide uppercase">Recent Photos</h2>
<div class="grid grid-cols-5 gap-2" @if($hasProcessing) wire:poll.2000ms @endif>
@foreach ($recentPhotos as $photo)
@ -103,20 +106,73 @@
const flash = document.getElementById('flash');
const countdown = document.getElementById('countdown');
const countdownNum = document.getElementById('countdown-num');
const cameraSelectWrap = document.getElementById('camera-select-wrap');
const cameraSelect = document.getElementById('camera-select');
const CAMERA_STORAGE_KEY = 'photobox_camera_id';
function videoConstraints(deviceId) {
const base = { width: { ideal: 4096 }, height: { ideal: 2160 } };
return deviceId ? { ...base, deviceId: { exact: deviceId } } : base;
}
async function startCamera(deviceId) {
if (video.srcObject) {
video.srcObject.getTracks().forEach(track => track.stop());
}
async function startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: { ideal: 4096 }, height: { ideal: 2160 } },
video: videoConstraints(deviceId),
audio: false,
});
video.srcObject = stream;
} catch {
btn.disabled = false;
} catch (err) {
if (deviceId && err.name === 'OverconstrainedError') {
localStorage.removeItem(CAMERA_STORAGE_KEY);
return startCamera(null);
}
$wire.set('status', 'Camera access denied or unavailable.');
btn.disabled = true;
}
}
async function populateCameraList() {
let devices;
try {
devices = await navigator.mediaDevices.enumerateDevices();
} catch {
return;
}
const cameras = devices.filter(d => d.kind === 'videoinput');
if (cameras.length < 2) {
cameraSelectWrap.classList.add('hidden');
return;
}
cameraSelect.innerHTML = '';
cameras.forEach((cam, index) => {
const option = document.createElement('option');
option.value = cam.deviceId;
option.textContent = cam.label || `Camera ${index + 1}`;
cameraSelect.appendChild(option);
});
const storedId = localStorage.getItem(CAMERA_STORAGE_KEY);
if (storedId && cameras.some(cam => cam.deviceId === storedId)) {
cameraSelect.value = storedId;
}
cameraSelectWrap.classList.remove('hidden');
}
cameraSelect.addEventListener('change', () => {
localStorage.setItem(CAMERA_STORAGE_KEY, cameraSelect.value);
startCamera(cameraSelect.value);
});
function triggerFlash() {
flash.style.opacity = '0.8';
setTimeout(() => flash.style.opacity = '0', 150);
@ -151,7 +207,10 @@
await $wire.capture(imageData);
});
startCamera();
(async () => {
await startCamera(localStorage.getItem(CAMERA_STORAGE_KEY));
await populateCameraList();
})();
</script>
@endscript
</div>