Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-14 12:51:35 +00:00
parent f626134cd4
commit a0b7f71756
2 changed files with 349 additions and 0 deletions
+196
View File
@@ -0,0 +1,196 @@
import { useEffect, useRef, useState } from 'react';
import { Share2, Copy, Users, X, Loader2 } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { startBroadcast, type BroadcasterHandle } from '@/lib/webrtc/broadcaster';
import { toast } from 'sonner';
interface ShareButtonProps {
variant?: 'default' | 'compact';
}
export function ShareButton({ variant = 'default' }: ShareButtonProps) {
const [open, setOpen] = useState(false);
const [starting, setStarting] = useState(false);
const [handle, setHandle] = useState<BroadcasterHandle | null>(null);
const [viewerCount, setViewerCount] = useState(0);
const streamRef = useRef<MediaStream | null>(null);
const link = handle ? `${window.location.origin}/watch/${handle.code}` : '';
const start = async () => {
setStarting(true);
try {
// Try canvas.captureStream first (works inside the page, no permission prompt)
const canvas = document.querySelector('canvas') as HTMLCanvasElement | null;
let stream: MediaStream | null = null;
if (canvas && typeof canvas.captureStream === 'function') {
stream = canvas.captureStream(30);
}
// Fallback: getDisplayMedia (asks user permission, captures the browser tab)
if (!stream || stream.getVideoTracks().length === 0) {
stream = await navigator.mediaDevices.getDisplayMedia({
video: { frameRate: 30 },
audio: false,
});
}
streamRef.current = stream;
const h = await startBroadcast(stream);
setHandle(h);
h.onViewerCountChange(setViewerCount);
toast.success(`Sala criada: ${h.code}`);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
toast.error(`Falha ao iniciar transmissão: ${msg}`);
} finally {
setStarting(false);
}
};
const stop = async () => {
if (handle) await handle.stop();
if (streamRef.current) streamRef.current.getTracks().forEach((t) => t.stop());
streamRef.current = null;
setHandle(null);
setViewerCount(0);
toast.info('Transmissão encerrada');
};
// Cleanup on unmount
useEffect(() => {
return () => {
if (handle) handle.stop();
if (streamRef.current) streamRef.current.getTracks().forEach((t) => t.stop());
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const copyLink = async () => {
await navigator.clipboard.writeText(link);
toast.success('Link copiado');
};
const shareLink = async () => {
if (navigator.share) {
try {
await navigator.share({
title: 'TrackSteelXR — Sessão ao vivo',
text: 'Acompanhe a inspeção ao vivo:',
url: link,
});
} catch {/* user cancelled */}
} else {
copyLink();
}
};
const isCompact = variant === 'compact';
return (
<>
<Button
variant={handle ? 'default' : 'outline'}
size={isCompact ? 'sm' : 'sm'}
className={isCompact ? 'h-8 gap-1.5 text-[10px] font-mono' : 'gap-2'}
onClick={() => setOpen(true)}
title="Compartilhar tela ao vivo"
>
<Share2 className={isCompact ? 'h-3.5 w-3.5' : 'h-4 w-4'} />
{handle ? (
<>
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-500" />
</span>
LIVE {viewerCount > 0 && `· ${viewerCount}`}
</>
) : (
'Compartilhar'
)}
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 font-mono text-sm">
<Share2 className="h-4 w-4 text-primary" />
Compartilhar tela ao vivo
</DialogTitle>
</DialogHeader>
{!handle ? (
<div className="space-y-4">
<p className="text-xs text-muted-foreground leading-relaxed">
Gere um link público para que até 5 pessoas acompanhem sua sessão ao vivo
pelo celular ou PC. Não precisa de login. A sala expira em 4h.
</p>
<Button onClick={start} disabled={starting} className="w-full gap-2">
{starting ? (
<><Loader2 className="h-4 w-4 animate-spin" /> Iniciando</>
) : (
<><Share2 className="h-4 w-4" /> Gerar link de acesso</>
)}
</Button>
</div>
) : (
<div className="space-y-4">
<div className="flex justify-center">
<div className="rounded-lg bg-white p-3">
<QRCodeSVG value={link} size={160} level="M" />
</div>
</div>
<div className="rounded-lg border bg-muted/30 p-3 space-y-2">
<div className="flex items-center justify-between">
<span className="font-mono text-[10px] text-muted-foreground">CÓDIGO</span>
<span className="font-mono text-lg font-bold tracking-widest text-primary">
{handle.code}
</span>
</div>
<div className="flex items-center gap-2 rounded bg-background px-2 py-1.5 border">
<input
readOnly
value={link}
className="flex-1 bg-transparent text-[10px] font-mono outline-none"
/>
<Button size="icon" variant="ghost" className="h-6 w-6" onClick={copyLink}>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
<div className="flex items-center justify-between rounded-lg border bg-muted/30 px-3 py-2">
<div className="flex items-center gap-2 text-xs">
<Users className="h-3.5 w-3.5 text-primary" />
<span className="font-mono text-[11px]">
{viewerCount} {viewerCount === 1 ? 'espectador' : 'espectadores'}
</span>
</div>
<div className="flex items-center gap-1.5">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-500" />
</span>
<span className="font-mono text-[10px] text-red-500">LIVE</span>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<Button onClick={shareLink} className="gap-2">
<Share2 className="h-4 w-4" /> Enviar
</Button>
<Button onClick={stop} variant="destructive" className="gap-2">
<X className="h-4 w-4" /> Encerrar
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
</>
);
}