feat(*): send images from website

This commit is contained in:
h
2026-01-23 02:16:46 +01:00
parent 9f4dd8313e
commit b9c4296ca3
30 changed files with 1917 additions and 98 deletions

View File

@@ -0,0 +1,144 @@
<script lang="ts">
import { onMount } from 'svelte';
interface Props {
oncomplete: (base64: string, mediaType: string, thumbnailBase64: string) => void;
oncancel: () => void;
}
let { oncomplete, oncancel }: Props = $props();
let count = $state(3);
let videoElement: HTMLVideoElement | null = $state(null);
let stream: MediaStream | null = $state(null);
let error: string | null = $state(null);
let cancelled = false;
let countdownInterval: ReturnType<typeof setInterval> | null = null;
async function startCamera() {
if (cancelled) return;
if (!navigator.mediaDevices?.getUserMedia) {
error = 'Camera not supported (requires HTTPS)';
return;
}
try {
const constraints: MediaStreamConstraints = {
video: {
facingMode: { ideal: 'environment' },
width: { ideal: 1920 },
height: { ideal: 1080 }
},
audio: false
};
stream = await navigator.mediaDevices.getUserMedia(constraints);
if (videoElement && !cancelled) {
videoElement.srcObject = stream;
startCountdown();
}
} catch (e) {
error = e instanceof Error ? e.message : 'Camera access denied';
}
}
function stopCamera() {
if (countdownInterval) {
clearInterval(countdownInterval);
countdownInterval = null;
}
if (stream) {
stream.getTracks().forEach((track) => track.stop());
stream = null;
}
}
function startCountdown() {
countdownInterval = setInterval(() => {
if (cancelled) {
stopCamera();
return;
}
count--;
if (count === 0) {
if (countdownInterval) clearInterval(countdownInterval);
capture();
}
}, 1000);
}
function capture() {
if (cancelled || !videoElement) {
stopCamera();
return;
}
const canvas = document.createElement('canvas');
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
const ctx = canvas.getContext('2d');
if (!ctx) {
stopCamera();
oncancel();
return;
}
ctx.drawImage(videoElement, 0, 0);
const base64 = canvas.toDataURL('image/jpeg', 0.85).split(',')[1];
const mediaType = 'image/jpeg';
const thumbMaxSize = 800;
const scale = Math.min(thumbMaxSize / canvas.width, thumbMaxSize / canvas.height, 1);
const thumbCanvas = document.createElement('canvas');
thumbCanvas.width = Math.round(canvas.width * scale);
thumbCanvas.height = Math.round(canvas.height * scale);
const thumbCtx = thumbCanvas.getContext('2d');
if (thumbCtx) {
thumbCtx.drawImage(canvas, 0, 0, thumbCanvas.width, thumbCanvas.height);
}
const thumbnailBase64 = thumbCanvas.toDataURL('image/jpeg', 0.7).split(',')[1];
stopCamera();
oncomplete(base64, mediaType, thumbnailBase64);
}
function handleCancel() {
cancelled = true;
stopCamera();
oncancel();
}
onMount(() => {
startCamera();
return () => {
cancelled = true;
stopCamera();
};
});
</script>
<div class="fixed inset-0 z-50 flex flex-col bg-black" data-camera-ui>
{#if error}
<div class="flex flex-1 flex-col items-center justify-center p-4">
<p class="mb-4 text-center text-sm text-red-400">{error}</p>
<button onclick={handleCancel} class="rounded bg-neutral-700 px-4 py-2 text-sm text-white">
Close
</button>
</div>
{:else}
<div class="relative flex-1">
<video bind:this={videoElement} autoplay playsinline muted class="h-full w-full object-cover"
></video>
<div class="absolute inset-0 flex items-center justify-center">
<span class="text-8xl font-bold text-white drop-shadow-lg">{count}</span>
</div>
</div>
<div class="p-4 text-center">
<button onclick={handleCancel} class="text-sm text-neutral-400">Cancel</button>
</div>
{/if}
</div>