1 Commits

Author SHA1 Message Date
MechaCat02
25f4fb1810 feat: implement camera capture step
Add in-app camera capture to the upload flow. Guests can now take photos
and record videos directly via getUserMedia without leaving the app.
The captured media is immediately queued through the existing IndexedDB
upload pipeline alongside library-picked files.

- CameraCapture.svelte: fullscreen overlay with live preview, photo
  capture (JPEG via canvas), video recording (WebM/MP4 via MediaRecorder),
  front/back camera toggle, recording timer, and permission-denied error state
- Upload page: side-by-side "Gallery" and "Camera" pickers; shared
  caption/hashtags fields apply to both sources; Blob→File conversion
  with timestamped filename before enqueue
- .env.test: reference environment config for local testing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 20:20:51 +02:00
2 changed files with 52 additions and 19 deletions

View File

@@ -8,8 +8,8 @@
let { oncapture, onclose }: Props = $props();
let videoEl: HTMLVideoElement;
let canvasEl: HTMLCanvasElement;
let videoEl: HTMLVideoElement = $state()!;
let canvasEl: HTMLCanvasElement = $state()!;
let stream: MediaStream | null = $state(null);
let facingMode = $state<'environment' | 'user'>('environment');
let recording = $state(false);

View File

@@ -3,11 +3,13 @@
import { getToken } from '$lib/auth';
import { addToQueue, loadQueue } from '$lib/upload-queue';
import UploadQueue from '$lib/components/UploadQueue.svelte';
import CameraCapture from '$lib/components/CameraCapture.svelte';
import { onMount } from 'svelte';
let caption = $state('');
let hashtags = $state('');
let fileInput: HTMLInputElement;
let showCamera = $state(false);
onMount(() => {
if (!getToken()) {
@@ -30,8 +32,23 @@
hashtags = '';
if (fileInput) fileInput.value = '';
}
async function handleCapture(blob: Blob, type: 'photo' | 'video') {
const ext = type === 'photo' ? 'jpg' : blob.type.includes('mp4') ? 'mp4' : 'webm';
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `${type}_${timestamp}.${ext}`;
const file = new File([blob], fileName, { type: blob.type });
await addToQueue(file, caption, hashtags);
}
</script>
{#if showCamera}
<CameraCapture
oncapture={handleCapture}
onclose={() => (showCamera = false)}
/>
{/if}
<div class="min-h-screen bg-gray-50 p-4">
<div class="mx-auto max-w-lg">
<div class="mb-6 flex items-center justify-between">
@@ -40,23 +57,39 @@
</div>
<div class="rounded-lg border border-gray-200 bg-white p-4">
<label
class="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-8 transition hover:border-blue-400 hover:bg-blue-50"
>
<svg class="mb-2 h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 16V4m0 0l-4 4m4-4l4 4M4 20h16" />
</svg>
<span class="text-sm font-medium text-gray-600">Fotos oder Videos auswählen</span>
<span class="mt-1 text-xs text-gray-400">Mehrere Dateien möglich</span>
<input
bind:this={fileInput}
type="file"
accept="image/*,video/*"
multiple
class="hidden"
onchange={handleFiles}
/>
</label>
<div class="grid grid-cols-2 gap-3">
<!-- File picker -->
<label
class="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-6 transition hover:border-blue-400 hover:bg-blue-50"
>
<svg class="mb-2 h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 16V4m0 0l-4 4m4-4l4 4M4 20h16" />
</svg>
<span class="text-center text-sm font-medium text-gray-600">Galerie</span>
<span class="mt-1 text-center text-xs text-gray-400">Mehrere Dateien</span>
<input
bind:this={fileInput}
type="file"
accept="image/*,video/*"
multiple
class="hidden"
onchange={handleFiles}
/>
</label>
<!-- Camera button -->
<button
onclick={() => (showCamera = true)}
class="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-6 transition hover:border-blue-400 hover:bg-blue-50"
>
<svg class="mb-2 h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span class="text-sm font-medium text-gray-600">Kamera</span>
<span class="mt-1 text-xs text-gray-400">Foto & Video</span>
</button>
</div>
<div class="mt-4 space-y-3">
<input