feat: implement barcode search configuration in 'Almoxarifado', integrating multiple external APIs for enhanced product information retrieval and improving user experience with new modals for data handling
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Image, Upload, X } from 'lucide-svelte';
|
||||
import { Image as ImageIcon, Upload, X, Camera } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
value?: string | null;
|
||||
@@ -20,6 +20,10 @@
|
||||
let preview = $state<string | null>(value);
|
||||
let error = $state<string | null>(null);
|
||||
let inputElement: HTMLInputElement | null = null;
|
||||
let showCamera = $state(false);
|
||||
let videoElement: HTMLVideoElement | null = null;
|
||||
let stream: MediaStream | null = null;
|
||||
let capturing = $state(false);
|
||||
|
||||
function handleFileSelect(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
@@ -73,7 +77,7 @@
|
||||
maxHeight: number
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const img = new window.Image();
|
||||
img.onload = () => {
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
@@ -123,8 +127,170 @@
|
||||
inputElement?.click();
|
||||
}
|
||||
|
||||
async function openCamera() {
|
||||
// Se já estiver inicializando ou já tiver stream, não fazer nada
|
||||
if (stream) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Primeiro, abrir o modal
|
||||
showCamera = true;
|
||||
capturing = false;
|
||||
error = null;
|
||||
|
||||
// Aguardar o próximo tick para garantir que o DOM foi atualizado
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
|
||||
try {
|
||||
// Verificar se a API está disponível
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
error = 'Câmera não disponível neste dispositivo ou navegador não suporta acesso à câmera';
|
||||
showCamera = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Aguardar o elemento de vídeo estar disponível no DOM
|
||||
let attempts = 0;
|
||||
while (!videoElement && attempts < 30) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (!videoElement) {
|
||||
throw new Error('Elemento de vídeo não encontrado no DOM');
|
||||
}
|
||||
|
||||
// Solicitar acesso à câmera
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: 'environment', // Câmera traseira por padrão
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 }
|
||||
}
|
||||
});
|
||||
|
||||
// Atribuir stream ao vídeo
|
||||
videoElement.srcObject = stream;
|
||||
|
||||
// Aguardar o vídeo estar pronto e começar a reproduzir
|
||||
await videoElement.play();
|
||||
|
||||
// Aguardar metadata estar carregado
|
||||
if (videoElement.readyState < 2) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (!videoElement) {
|
||||
reject(new Error('Elemento de vídeo não encontrado'));
|
||||
return;
|
||||
}
|
||||
|
||||
const onLoadedMetadata = () => {
|
||||
videoElement?.removeEventListener('loadedmetadata', onLoadedMetadata);
|
||||
videoElement?.removeEventListener('error', onError);
|
||||
resolve();
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
videoElement?.removeEventListener('loadedmetadata', onLoadedMetadata);
|
||||
videoElement?.removeEventListener('error', onError);
|
||||
reject(new Error('Erro ao carregar vídeo'));
|
||||
};
|
||||
|
||||
videoElement.addEventListener('loadedmetadata', onLoadedMetadata, { once: true });
|
||||
videoElement.addEventListener('error', onError, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
capturing = true;
|
||||
} catch (err) {
|
||||
console.error('Erro ao acessar câmera:', err);
|
||||
let errorMessage = 'Erro ao acessar câmera';
|
||||
|
||||
if (err instanceof Error) {
|
||||
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
|
||||
errorMessage = 'Permissão de acesso à câmera negada. Por favor, permita o acesso à câmera nas configurações do navegador.';
|
||||
} else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
|
||||
errorMessage = 'Nenhuma câmera encontrada no dispositivo.';
|
||||
} else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') {
|
||||
errorMessage = 'Câmera está sendo usada por outro aplicativo.';
|
||||
} else {
|
||||
errorMessage = err.message || errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
error = errorMessage;
|
||||
showCamera = false;
|
||||
capturing = false;
|
||||
stopCamera();
|
||||
}
|
||||
}
|
||||
|
||||
function stopCamera() {
|
||||
if (stream) {
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
stream = null;
|
||||
}
|
||||
if (videoElement) {
|
||||
videoElement.srcObject = null;
|
||||
}
|
||||
capturing = false;
|
||||
}
|
||||
|
||||
function closeCamera() {
|
||||
stopCamera();
|
||||
showCamera = false;
|
||||
error = null;
|
||||
}
|
||||
|
||||
async function capturePhoto() {
|
||||
if (!videoElement) return;
|
||||
|
||||
try {
|
||||
// Criar canvas para capturar o frame
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = videoElement.videoWidth;
|
||||
canvas.height = videoElement.videoHeight;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
error = 'Não foi possível criar contexto do canvas';
|
||||
return;
|
||||
}
|
||||
|
||||
// Desenhar o frame atual do vídeo no canvas
|
||||
ctx.drawImage(videoElement, 0, 0);
|
||||
|
||||
// Converter para base64
|
||||
const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
|
||||
|
||||
// Redimensionar e processar
|
||||
const resized = await resizeImage(dataUrl, maxWidth, maxHeight);
|
||||
preview = resized;
|
||||
value = resized;
|
||||
if (onChange) {
|
||||
onChange(resized);
|
||||
}
|
||||
|
||||
// Fechar câmera
|
||||
closeCamera();
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Erro ao capturar foto';
|
||||
console.error('Erro ao capturar foto:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Sincronizar preview com value sempre que value mudar
|
||||
$effect(() => {
|
||||
preview = value;
|
||||
if (value !== preview) {
|
||||
preview = value;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Limpar stream quando o componente for desmontado
|
||||
$effect(() => {
|
||||
return () => {
|
||||
stopCamera();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -151,23 +317,34 @@
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="border-2 border-dashed border-base-300 rounded-lg p-8 text-center cursor-pointer hover:border-primary transition-colors"
|
||||
onclick={triggerFileInput}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
triggerFileInput();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Upload class="h-12 w-12 mx-auto mb-4 text-base-content/40" />
|
||||
<p class="text-base-content/70 font-medium mb-2">Clique para fazer upload da imagem</p>
|
||||
<p class="text-sm text-base-content/50">
|
||||
PNG, JPG ou GIF até {maxSizeMB}MB
|
||||
</p>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div
|
||||
class="border-2 border-dashed border-base-300 rounded-lg p-8 text-center cursor-pointer hover:border-primary transition-colors"
|
||||
onclick={triggerFileInput}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
triggerFileInput();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Upload class="h-12 w-12 mx-auto mb-4 text-base-content/40" />
|
||||
<p class="text-base-content/70 font-medium mb-2">Clique para fazer upload da imagem</p>
|
||||
<p class="text-sm text-base-content/50">
|
||||
PNG, JPG ou GIF até {maxSizeMB}MB
|
||||
</p>
|
||||
</div>
|
||||
<div class="divider text-sm">ou</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-primary w-full"
|
||||
onclick={openCamera}
|
||||
>
|
||||
<Camera class="h-5 w-5" />
|
||||
Capturar da Câmera
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -178,17 +355,89 @@
|
||||
{/if}
|
||||
|
||||
{#if preview}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline btn-primary mt-4"
|
||||
onclick={triggerFileInput}
|
||||
>
|
||||
<Image class="h-4 w-4" />
|
||||
Alterar Imagem
|
||||
</button>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline btn-primary flex-1"
|
||||
onclick={triggerFileInput}
|
||||
>
|
||||
<ImageIcon class="h-4 w-4" />
|
||||
Alterar Imagem
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline btn-primary flex-1"
|
||||
onclick={openCamera}
|
||||
>
|
||||
<Camera class="h-4 w-4" />
|
||||
Capturar Foto
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Modal da Câmera -->
|
||||
{#if showCamera}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80" onclick={closeCamera}>
|
||||
<div
|
||||
class="bg-base-100 rounded-lg shadow-2xl p-6 max-w-2xl w-full mx-4"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-xl font-bold">Capturar Foto</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost"
|
||||
onclick={closeCamera}
|
||||
aria-label="Fechar câmera"
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative bg-black rounded-lg overflow-hidden mb-4" style="aspect-ratio: 4/3; min-height: 300px;">
|
||||
{#if showCamera}
|
||||
<video
|
||||
bind:this={videoElement}
|
||||
autoplay
|
||||
playsinline
|
||||
muted
|
||||
class="w-full h-full object-cover"
|
||||
style="transform: scaleX(-1); opacity: {capturing ? '1' : '0'}; transition: opacity 0.3s;"
|
||||
></video>
|
||||
{/if}
|
||||
{#if !capturing}
|
||||
<div class="flex items-center justify-center h-full absolute inset-0 z-10">
|
||||
<div class="text-center">
|
||||
<span class="loading loading-spinner loading-lg text-primary mb-2"></span>
|
||||
<p class="text-base-content/70 text-sm">Iniciando câmera...</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost"
|
||||
onclick={closeCamera}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
onclick={capturePhoto}
|
||||
disabled={!capturing}
|
||||
>
|
||||
<Camera class="h-5 w-5" />
|
||||
Capturar Foto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.image-upload {
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user