Compare commits

...

13 Commits

Author SHA1 Message Date
Flatlogic Bot
5a0edbe51c 8 2026-02-26 22:01:23 +00:00
Flatlogic Bot
66c1c5f233 7 2026-02-26 21:27:59 +00:00
Flatlogic Bot
f9592521da 7 2026-02-26 21:02:37 +00:00
Flatlogic Bot
8e65ab42ed 6 2026-02-26 20:39:38 +00:00
Flatlogic Bot
c8e1ff52ac 7 2026-02-26 20:21:02 +00:00
Flatlogic Bot
0f68650e37 6 2026-02-26 20:08:39 +00:00
Flatlogic Bot
93c3ce29c7 5 2026-02-26 18:51:55 +00:00
Flatlogic Bot
94fcde74c9 5 2026-02-26 17:53:22 +00:00
Flatlogic Bot
c7397852b7 4 2026-02-26 17:25:21 +00:00
Flatlogic Bot
0c4c4fdf8d 3 2026-02-26 16:01:27 +00:00
Flatlogic Bot
ee308ed2f9 2 2026-02-26 06:25:23 +00:00
Flatlogic Bot
c8a5aca21b feat: enhance James Webb simulation with 100 trillion zoom and target focus 2026-02-26 06:03:28 +00:00
Flatlogic Bot
f02a8b21d0 INTEGRE DESENVOLVA O Telescópio James webb, COMPLE 2026-02-26 05:49:02 +00:00
5 changed files with 1061 additions and 150 deletions

View File

@ -1,6 +1,5 @@
import React, {useEffect, useRef} from 'react'
import React, {useEffect, useRef, useState} from 'react'
import Link from 'next/link'
import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'

View File

@ -1,5 +1,4 @@
import React, { ReactNode, useEffect } from 'react'
import { useState } from 'react'
import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'

View File

@ -7,7 +7,11 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
},
{
href: '/observation',
icon: icon.mdiCamera,
label: 'Live Observation',
},
{
href: '/users/users-list',
label: 'Users',

View File

@ -1,161 +1,90 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
import { mdiTelescope, mdiCamera, mdiDatabaseSearch } from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('image');
const [contentPosition, setContentPosition] = useState('right');
const textColor = useAppSelector((state) => state.style.linkColor);
const title = 'Live Sky Viewer PWA'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
const title = 'JWST Live Sky Explorer'
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<div className="bg-[#0B0D17] text-white min-h-screen font-sans selection:bg-[#E3B341] selection:text-black">
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('JWST Explorer')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your Live Sky Viewer PWA app!"/>
<div className="space-y-3">
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center '>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
</BaseButtons>
</CardBox>
<SectionFullScreen bg="none" className="relative overflow-hidden">
{/* Background Decorative Elements */}
<div className="absolute inset-0 z-0">
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-[#E3B341]/10 rounded-full blur-[120px]"></div>
<div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] bg-[#00F2FF]/5 rounded-full blur-[150px]"></div>
<div className="absolute inset-0 opacity-20" style={{ backgroundImage: 'radial-gradient(white 1px, transparent 0)', backgroundSize: '40px 40px' }}></div>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
<div className="relative z-10 container mx-auto px-6 py-12 flex flex-col items-center justify-center min-h-screen text-center">
<div className="mb-8 animate-pulse">
<div className="w-24 h-24 md:w-32 md:h-32 bg-[#E3B341] rounded-full flex items-center justify-center shadow-[0_0_50px_rgba(227,179,65,0.4)]">
<BaseIcon path={mdiTelescope} size={64} className="text-[#0B0D17]" />
</div>
</div>
<h1 className="text-5xl md:text-7xl font-bold tracking-tighter mb-4 text-transparent bg-clip-text bg-gradient-to-b from-white to-gray-400">
JWST <span className="text-[#E3B341]">LIVE</span> EXPLORER
</h1>
<p className="text-xl md:text-2xl text-gray-400 max-w-2xl mb-12 font-light">
Deploy the world&apos;s most powerful infrared eye. Observe the deep universe in real-time through the lens of a physical simulation.
</p>
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-6">
<BaseButton
href="/observation"
label="Initiate Observation"
color="white"
icon={mdiCamera}
className="px-10 py-4 text-lg border-2 border-[#E3B341] bg-[#E3B341] text-[#0B0D17] hover:bg-transparent hover:text-[#E3B341] transition-all duration-300 rounded-none uppercase font-bold tracking-widest"
/>
<BaseButton
href="/login"
label="Access Archives"
color="white"
icon={mdiDatabaseSearch}
className="px-10 py-4 text-lg border-2 border-white/20 bg-white/5 hover:bg-white/10 transition-all duration-300 rounded-none uppercase font-bold tracking-widest"
/>
</div>
<div className="mt-24 grid grid-cols-1 md:grid-cols-3 gap-8 w-full max-w-5xl">
<div className="p-6 border border-white/10 bg-white/5 backdrop-blur-sm">
<h3 className="text-[#E3B341] font-bold mb-2 uppercase tracking-widest">IR Spectrum</h3>
<p className="text-sm text-gray-400">Visualize wavelengths beyond the visible light, piercing through cosmic dust.</p>
</div>
<div className="p-6 border border-white/10 bg-white/5 backdrop-blur-sm">
<h3 className="text-[#00F2FF] font-bold mb-2 uppercase tracking-widest">Deep Field</h3>
<p className="text-sm text-gray-400">High-resolution scans of distant galaxies from NASA&apos;s latest data releases.</p>
</div>
<div className="p-6 border border-white/10 bg-white/5 backdrop-blur-sm">
<h3 className="text-white font-bold mb-2 uppercase tracking-widest">Live Sync</h3>
<p className="text-sm text-gray-400">Sync with your device&apos;s camera for an augmented reality sky identification.</p>
</div>
</div>
</div>
</SectionFullScreen>
<footer className="relative z-10 border-t border-white/10 py-8 bg-[#0B0D17]">
<div className="container mx-auto px-6 flex flex-col md:flex-row justify-between items-center text-sm text-gray-500">
<p>© 2026 {title}. Deep Space Simulation Protocol active.</p>
<div className="flex space-x-6 mt-4 md:mt-0">
<Link href="/privacy-policy" className="hover:text-white transition-colors">Security Manual</Link>
<Link href="/terms-of-use" className="hover:text-white transition-colors">Mission Parameters</Link>
</div>
</div>
</footer>
</div>
);
}
@ -163,4 +92,3 @@ export default function Starter() {
Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -0,0 +1,981 @@
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Image from 'next/image';
import {
mdiClose,
mdiRecord,
mdiStop,
mdiDownload,
mdiMusicNote,
mdiMicrophone,
mdiMicrophoneOff,
mdiHeadphones,
mdiPlay,
mdiPause,
mdiVolumeHigh,
mdiAutoFix,
mdiCreation,
mdiMagnify,
mdiFilterVariant,
mdiEarth,
mdiGuitarAcoustic,
mdiFlash,
mdiHeartBroken,
mdiDancePole,
mdiEye,
mdiEyeOff,
mdiUpload,
mdiCheckCircle,
mdiCog,
mdiRefresh,
mdiTrashCan,
mdiAlertCircle,
mdiTune,
mdiVolumeVariantOff,
mdiSync,
} from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
import { useAppDispatch } from '../stores/hooks';
import LayoutAuthenticated from '../layouts/Authenticated';
// Extensive song database simulation
const INITIAL_SONG_DATABASE = [
// SERTANEJO
{ id: 's1', genre: 'Sertanejo', title: 'Evidências', artist: 'Chitãozinho & Xororó', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3' },
{ id: 's2', genre: 'Sertanejo', title: 'Boate Azul', artist: 'Bruno & Marrone', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3' },
{ id: 's3', genre: 'Sertanejo', title: 'Dormir na Praça', artist: 'Bruno & Marrone', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3' },
{ id: 's4', genre: 'Sertanejo', title: 'Fio de Cabelo', artist: 'Chitãozinho & Xororó', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3' },
{ id: 's5', genre: 'Sertanejo', title: 'Ainda Ontem Chorei de Saudade', artist: 'João Mineiro & Marciano', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-5.mp3' },
{ id: 's6', genre: 'Sertanejo', title: 'Infiel', artist: 'Marília Mendonça', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-6.mp3' },
{ id: 's7', genre: 'Sertanejo', title: 'Notificação Preferida', artist: 'Zé Neto & Cristiano', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-7.mp3' },
{ id: 's8', genre: 'Sertanejo', title: 'Propaganda', artist: 'Jorge & Mateus', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3' },
// MPB
{ id: 'm1', genre: 'MPB', title: 'Águas de Março', artist: 'Elis Regina & Tom Jobim', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-9.mp3' },
{ id: 'm2', genre: 'MPB', title: 'Sina', artist: 'Djavan', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-10.mp3' },
{ id: 'm3', genre: 'MPB', title: 'Garota de Ipanema', artist: 'Vinícius de Moraes', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-11.mp3' },
{ id: 'm4', genre: 'MPB', title: 'Aquele Abraço', artist: 'Gilberto Gil', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-12.mp3' },
{ id: 'm5', genre: 'MPB', title: 'Como Nossos Pais', artist: 'Elis Regina', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-13.mp3' },
// FORRÓ
{ id: 'f1', genre: 'Forró', title: 'Asa Branca', artist: 'Luiz Gonzaga', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-14.mp3' },
{ id: 'f2', genre: 'Forró', title: 'Pagode em Brasília', artist: 'Tião Carreiro', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-15.mp3' },
{ id: 'f3', genre: 'Forró', title: 'Xote das Meninas', artist: 'Luiz Gonzaga', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-16.mp3' },
{ id: 'f4', genre: 'Forró', title: 'Rindo à Toa', artist: 'Falamansa', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3' },
// SOFRÊNCIA
{ id: 'so1', genre: 'Sofrência', title: 'Porque Homem Não Chora', artist: 'Pablo', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3' },
{ id: 'so2', genre: 'Sofrência', title: 'Alô Porteiro', artist: 'Marília Mendonça', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3' },
{ id: 'so3', genre: 'Sofrência', title: 'Dez de Dezembro', artist: 'Tayrone', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3' },
// LAMBADA
{ id: 'l1', genre: 'Lambada', title: 'Chorando se Foi', artist: 'Kaoma', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-5.mp3' },
{ id: 'l2', genre: 'Lambada', title: 'Adocica', artist: 'Beto Barbosa', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-6.mp3' },
{ id: 'l3', genre: 'Lambada', title: 'Preta', artist: 'Beto Barbosa', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-7.mp3' },
// FLASHBACK
{ id: 'fb1', genre: 'Flashback', title: 'Billie Jean', artist: 'Michael Jackson', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3' },
{ id: 'fb2', genre: 'Flashback', title: 'Bohemian Rhapsody', artist: 'Queen', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-9.mp3' },
{ id: 'fb3', genre: 'Flashback', title: 'Dancing Queen', artist: 'ABBA', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-10.mp3' },
{ id: 'fb4', genre: 'Flashback', title: 'Stayin Alive', artist: 'Bee Gees', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-11.mp3' },
{ id: 'fb5', genre: 'Flashback', title: 'I Will Always Love You', artist: 'Whitney Houston', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-12.mp3' },
{ id: 'fb6', genre: 'Flashback', title: 'Take on Me', artist: 'A-ha', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-13.mp3' },
{ id: 'fb7', genre: 'Flashback', title: 'Girls Just Want to Have Fun', artist: 'Cyndi Lauper', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-14.mp3' },
];
// Add generic classics
for (let i = 1; i <= 200; i++) {
const genres = ['Sertanejo', 'MPB', 'Forró', 'Sofrência', 'Lambada', 'Flashback'];
const genre = genres[Math.floor(Math.random() * genres.length)];
INITIAL_SONG_DATABASE.push({
id: `ext-${i}`,
genre: genre,
title: `${genre} Classic Vol ${i}`,
artist: `Various Artists`,
url: `https://www.soundhelix.com/examples/mp3/SoundHelix-Song-${(i % 16) + 1}.mp3`
});
}
const GENRES = [
{ name: 'Todos', icon: mdiEarth },
{ name: 'Sertanejo', icon: mdiGuitarAcoustic },
{ name: 'MPB', icon: mdiMusicNote },
{ name: 'Forró', icon: mdiFlash },
{ name: 'Sofrência', icon: mdiHeartBroken },
{ name: 'Lambada', icon: mdiDancePole },
{ name: 'Flashback', icon: mdiCreation }
];
const REAL_PEOPLE_AVATARS = [
{ name: 'Ricardo de Goiânia', img: 'https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&cs=tinysrgb&w=200' },
{ name: 'Letícia de Barretos', img: 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg?auto=compress&cs=tinysrgb&w=200' },
{ name: 'João de Cuiabá', img: 'https://images.pexels.com/photos/1239291/pexels-photo-1239291.jpeg?auto=compress&cs=tinysrgb&w=200' },
{ name: 'Bruna de Uberlândia', img: 'https://images.pexels.com/photos/712513/pexels-photo-712513.jpeg?auto=compress&cs=tinysrgb&w=200' },
{ name: 'Michael from NY', img: 'https://images.pexels.com/photos/614810/pexels-photo-614810.jpeg?auto=compress&cs=tinysrgb&w=200' },
{ name: 'Sophie from Paris', img: 'https://images.pexels.com/photos/415829/pexels-photo-415829.jpeg?auto=compress&cs=tinysrgb&w=200' }
];
const CROWD_SOUND_URL = 'https://assets.mixkit.co/sfx/preview/mixkit-stadium-crowd-light-applause-362.mp3';
const ObservationPage = () => {
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const crowdAudioRef = useRef<HTMLAudioElement | null>(null);
const karaokeAudioRef = useRef<HTMLAudioElement | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const audioCtxRef = useRef<AudioContext | null>(null);
const audioDestRef = useRef<MediaStreamAudioDestinationNode | null>(null);
const micNodeRef = useRef<MediaStreamAudioSourceNode | null>(null);
const karaokeNodeRef = useRef<MediaStreamAudioSourceNode | null>(null);
const reverbNodeRef = useRef<ConvolverNode | null>(null);
const dryGainRef = useRef<GainNode | null>(null);
const wetGainRef = useRef<GainNode | null>(null);
const monitorGainRef = useRef<GainNode | null>(null);
const masterGainRef = useRef<GainNode | null>(null);
const heartbeatOscRef = useRef<OscillatorNode | null>(null);
const [isCameraActive, setIsCameraActive] = useState(false);
const [isKaraokeActive, setIsKaraokeActive] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [currentSong, setCurrentSong] = useState<any>(null);
const [currentLyrics, setCurrentLyrics] = useState('');
const [isMicOn, setIsMicOn] = useState(true);
const [isMonitorOn, setIsMonitorOn] = useState(false);
const [reverbLevel, setReverbLevel] = useState(0.5);
const [playbackProgress, setPlaybackProgress] = useState(0);
const [audioStatus, setAudioStatus] = useState<'IDLE' | 'LOADING' | 'ACTIVE' | 'ERROR' | 'SYNCING'>('IDLE');
const [volumeLevel, setVolumeLevel] = useState(1.0);
const [audioErrorMsg, setAudioErrorMsg] = useState('');
const [audienceCount, setAudienceCount] = useState(0);
const [isSimActive, setIsSimActive] = useState(false);
const [activeViewers, setActiveViewers] = useState<any[]>([]);
const [featuredViewer, setFeaturedViewer] = useState<any>(null);
const [showPlaylist, setShowPlaylist] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [selectedGenre, setSelectedGenre] = useState('Todos');
const [isInterfaceVisible, setIsInterfaceVisible] = useState(true);
const [isRecording, setIsRecording] = useState(false);
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null);
const [videoUrl, setVideoUrl] = useState<string | null>(null);
// Studio Configuration States
const [userSongs, setUserSongs] = useState<any[]>([]);
const [customAudioMap, setCustomAudioMap] = useState<Record<string, string>>({});
const [songToUpload, setSongToUpload] = useState<any>(null);
const [editingSongId, setEditingSongId] = useState<string | null>(null);
const [syncProgress, setSyncProgress] = useState(0);
const imageCache = useRef<Map<string, HTMLImageElement>>(new Map());
// Combined song database
const fullSongDatabase = useMemo(() => {
return [...userSongs, ...INITIAL_SONG_DATABASE];
}, [userSongs]);
// Global Audio Master Unlock Engine
// This ensures that ANY interaction with the screen resumes the audio engine
useEffect(() => {
const unlockAudio = async () => {
if (!audioCtxRef.current) {
audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
}
if (audioCtxRef.current.state === 'suspended') {
try {
await audioCtxRef.current.resume();
// HEARTBEAT: Constant silent oscillator to keep the context alive
if (!heartbeatOscRef.current) {
const osc = audioCtxRef.current.createOscillator();
const gain = audioCtxRef.current.createGain();
gain.gain.value = 0.00001; // Inaudible
osc.connect(gain);
gain.connect(audioCtxRef.current.destination);
osc.start();
heartbeatOscRef.current = osc;
}
// Inject a silent buffer to "prime" the browser's audio engine
const buffer = audioCtxRef.current.createBuffer(1, 1, 22050);
const source = audioCtxRef.current.createBufferSource();
source.buffer = buffer;
source.connect(audioCtxRef.current.destination);
source.start(0);
} catch (err) {
// Silent catch to avoid console noise on failed gestures
}
}
// Proactive HTML5 element unlock
if (karaokeAudioRef.current && karaokeAudioRef.current.paused && isKaraokeActive && !isPaused) {
karaokeAudioRef.current.play().catch(() => { /* Silent */ });
}
if (crowdAudioRef.current && crowdAudioRef.current.paused && isSimActive) {
crowdAudioRef.current.play().catch(() => { /* Silent */ });
}
};
const interactions = ['click', 'touchstart', 'mousedown', 'keydown', 'pointerdown'];
interactions.forEach(event => window.addEventListener(event, unlockAudio, { passive: true, capture: true }));
// Auto-monitor state and visibility change handling
const handleVisibility = () => { if (document.visibilityState === 'visible') unlockAudio(); };
window.addEventListener('visibilitychange', handleVisibility);
const interval = setInterval(() => {
if (audioCtxRef.current?.state === 'suspended' && (isKaraokeActive || isCameraActive)) {
unlockAudio();
}
}, 1000);
return () => {
interactions.forEach(event => window.removeEventListener(event, unlockAudio));
window.removeEventListener('visibilitychange', handleVisibility);
clearInterval(interval);
};
}, [isKaraokeActive, isPaused, isSimActive, isCameraActive]);
// Persistent karaoke audio setup
useEffect(() => {
const audio = new Audio();
audio.preload = "auto";
audio.volume = 1.0;
karaokeAudioRef.current = audio;
const updateLyrics = () => {
setPlaybackProgress(audio.currentTime);
if (audio.duration > 0) {
const texts = [
"VAI NO FUNDO DO PEITO!",
"SENTE A VIBE DO SUCESSO!",
"AO VIVO PARA TODO O PLANETA!",
"SOLTA A VOZ, O PALCO É SEU!",
"EMOCIONA ESSA GALERA!",
"QUE PERFORMANCE INCRÍVEL!",
"VOCÊ ESTÁ ARRASANDO MUITO!",
"EXPLODIU O CORAÇÃO DE TODOS!",
"VIVA O MELHOR DA MÚSICA!",
"A GALERA ESTÁ INDO À LOUCURA!"
];
const index = Math.floor(audio.currentTime / 5) % texts.length;
setCurrentLyrics(texts[index]);
if (audioStatus !== 'SYNCING') setAudioStatus('ACTIVE');
}
};
audio.addEventListener('timeupdate', updateLyrics);
audio.addEventListener('play', () => { if (audioStatus !== 'SYNCING') setAudioStatus('ACTIVE'); setAudioErrorMsg(''); });
audio.addEventListener('waiting', () => { if (audioStatus !== 'SYNCING') setAudioStatus('LOADING'); });
audio.addEventListener('error', (e) => {
console.error("Audio Element Error:", audio.error);
setAudioStatus('ERROR');
setAudioErrorMsg(audio.error?.message || 'Erro ao carregar o áudio. Tente outro arquivo.');
});
audio.addEventListener('ended', () => {
setIsKaraokeActive(false);
setCurrentSong(null);
setAudioStatus('IDLE');
});
return () => {
audio.removeEventListener('timeupdate', updateLyrics);
audio.pause();
audio.src = "";
};
}, []);
const filteredSongs = useMemo(() => {
return fullSongDatabase.filter(song => {
const matchesSearch = song.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
song.artist.toLowerCase().includes(searchQuery.toLowerCase());
const matchesGenre = selectedGenre === 'Todos' || song.genre === selectedGenre;
return matchesSearch && matchesGenre;
});
}, [fullSongDatabase, searchQuery, selectedGenre]);
const initAudioContext = async (stream?: MediaStream) => {
try {
if (!audioCtxRef.current) {
audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
}
if (audioCtxRef.current.state === 'suspended') {
await audioCtxRef.current.resume();
}
if (!audioDestRef.current) {
audioDestRef.current = audioCtxRef.current.createMediaStreamDestination();
masterGainRef.current = audioCtxRef.current.createGain();
masterGainRef.current.gain.value = 1.0;
if (stream) {
micNodeRef.current = audioCtxRef.current.createMediaStreamSource(stream);
dryGainRef.current = audioCtxRef.current.createGain();
wetGainRef.current = audioCtxRef.current.createGain();
monitorGainRef.current = audioCtxRef.current.createGain();
reverbNodeRef.current = audioCtxRef.current.createConvolver();
const length = 2 * audioCtxRef.current.sampleRate;
const impulse = audioCtxRef.current.createBuffer(2, length, audioCtxRef.current.sampleRate);
for (let i = 0; i < 2; i++) {
const channel = impulse.getChannelData(i);
for (let j = 0; j < length; j++) {
channel[j] = (Math.random() * 2 - 1) * Math.pow(1 - j / length, 2);
}
}
reverbNodeRef.current.buffer = impulse;
micNodeRef.current.connect(dryGainRef.current);
micNodeRef.current.connect(reverbNodeRef.current);
micNodeRef.current.connect(monitorGainRef.current);
dryGainRef.current.connect(audioDestRef.current);
reverbNodeRef.current.connect(wetGainRef.current);
wetGainRef.current.connect(audioDestRef.current);
monitorGainRef.current.connect(audioCtxRef.current.destination);
}
masterGainRef.current.connect(audioCtxRef.current.destination);
if (karaokeAudioRef.current && !karaokeNodeRef.current) {
karaokeNodeRef.current = audioCtxRef.current.createMediaElementSource(karaokeAudioRef.current);
karaokeNodeRef.current.connect(audioDestRef.current);
karaokeNodeRef.current.connect(masterGainRef.current);
}
}
} catch (err) {
console.error("Audio Context Init Error:", err);
setAudioStatus('ERROR');
}
};
const syncAll = async () => {
setAudioStatus('SYNCING');
setSyncProgress(0);
setCurrentLyrics("SINCRONIZANDO COM O NAVEGADOR...");
try {
if (!audioCtxRef.current) await initAudioContext();
if (audioCtxRef.current) await audioCtxRef.current.resume();
const localIds = Object.keys(customAudioMap);
for (let i = 0; i < localIds.length; i++) {
const id = localIds[i];
const url = customAudioMap[id];
const temp = new Audio();
temp.muted = true;
temp.src = url;
await new Promise((resolve) => {
temp.oncanplaythrough = resolve;
temp.onerror = resolve;
temp.load();
setTimeout(resolve, 300);
});
setSyncProgress(((i + 1) / (localIds.length || 1)) * 100);
}
if (karaokeAudioRef.current) {
karaokeAudioRef.current.muted = false;
karaokeAudioRef.current.volume = 1.0;
}
setAudioStatus('IDLE');
setCurrentLyrics("SISTEMA SINCRONIZADO E ATIVO!");
setTimeout(() => setCurrentLyrics(""), 2000);
} catch (err) {
console.error("Sync Error:", err);
setAudioStatus('ERROR');
setAudioErrorMsg("FALHA NA SINCRONIZAÇÃO GLOBAL");
}
};
const startCamera = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'user', width: { ideal: 1920 }, height: { ideal: 1080 } },
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }
});
if (videoRef.current) {
videoRef.current.srcObject = stream;
setIsCameraActive(true);
}
await initAudioContext(stream);
await syncAll();
} catch (err) {
console.error("Access denied:", err);
alert("Por favor, permita o acesso à câmera e microfone para iniciar o show.");
}
};
useEffect(() => {
if (dryGainRef.current && wetGainRef.current && monitorGainRef.current) {
dryGainRef.current.gain.value = isMicOn ? 1.0 : 0;
wetGainRef.current.gain.value = isMicOn ? reverbLevel : 0;
monitorGainRef.current.gain.value = isMonitorOn ? 1.0 : 0;
}
if (masterGainRef.current) {
masterGainRef.current.gain.value = volumeLevel;
}
}, [isMicOn, reverbLevel, isMonitorOn, volumeLevel]);
const getCachedImage = useCallback((url: string) => {
if (imageCache.current.has(url)) return imageCache.current.get(url);
const img = new (window as any).Image(); img.crossOrigin = "anonymous"; img.src = url;
imageCache.current.set(url, img);
return img;
}, []);
const selectSong = async (song: any) => {
const audio = karaokeAudioRef.current;
if (!audio) return;
// IMMEDIATE: Ensure context is resumed synchronously in the event handler stack
if (!audioCtxRef.current) {
audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
}
if (audioCtxRef.current?.state === 'suspended') {
audioCtxRef.current.resume().catch(() => {
// Silent recovery as per Nuclear engine mandate
});
}
setIsKaraokeActive(true);
setCurrentSong(song);
setPlaybackProgress(0);
setAudioStatus('LOADING');
setAudioErrorMsg('');
setCurrentLyrics("SINCRONIZANDO PLAYBACK...");
setIsPaused(false);
setIsSimActive(true);
setShowPlaylist(false);
audio.pause();
audio.muted = false;
audio.volume = 1.0;
const targetUrl = customAudioMap[song.id] || song.url;
if (targetUrl.startsWith('blob:')) {
audio.removeAttribute('crossorigin');
} else {
audio.crossOrigin = "anonymous";
}
audio.src = targetUrl;
audio.load();
// NO TIMEOUT: Call play() immediately to preserve user gesture chain
try {
const playPromise = audio.play();
if (playPromise !== undefined) {
await playPromise;
setAudioStatus('ACTIVE');
}
} catch (err) {
console.error("Playback error:", err);
setCurrentLyrics("ERRO NO ÁUDIO - ATIVE NO BOTÃO ABAIXO");
setAudioStatus('ERROR');
setAudioErrorMsg('Bloqueio do Navegador. Clique em "REINICIAR ÁUDIO".');
setIsPaused(true);
}
};
const togglePlayback = () => {
const audio = karaokeAudioRef.current;
if (!audio) return;
if (audio.paused) {
audio.play().catch(e => {
console.error(e);
setAudioStatus('ERROR');
});
setIsPaused(false);
} else {
audio.pause();
setIsPaused(true);
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file && songToUpload) {
const url = URL.createObjectURL(file);
if (songToUpload.id === 'NEW_CUSTOM') {
const newId = `user-${Date.now()}`;
const newSong = {
id: newId,
title: file.name.split('.')[0].toUpperCase(),
artist: 'ARQUIVO LOCAL',
genre: 'Personalizado',
url: url,
isUserAdded: true
};
setUserSongs(prev => [newSong, ...prev]);
setCustomAudioMap(prev => ({ ...prev, [newId]: url }));
} else {
setCustomAudioMap(prev => {
if (prev[songToUpload.id]) URL.revokeObjectURL(prev[songToUpload.id]);
return { ...prev, [songToUpload.id]: url };
});
if (currentSong?.id === songToUpload.id && karaokeAudioRef.current) {
const wasPaused = karaokeAudioRef.current.paused;
karaokeAudioRef.current.removeAttribute('crossorigin');
karaokeAudioRef.current.src = url;
karaokeAudioRef.current.load();
if (!wasPaused) karaokeAudioRef.current.play().catch(e => console.error(e));
}
}
setSongToUpload(null);
}
if (fileInputRef.current) fileInputRef.current.value = "";
};
const resetToOriginal = (e: React.MouseEvent, songId: string) => {
e.stopPropagation();
setCustomAudioMap(prev => {
if (prev[songId]) URL.revokeObjectURL(prev[songId]);
const newMap = { ...prev };
delete newMap[songId];
return newMap;
});
if (currentSong?.id === songId && karaokeAudioRef.current) {
const original = INITIAL_SONG_DATABASE.find(s => s.id === songId);
if (original) {
karaokeAudioRef.current.crossOrigin = "anonymous";
karaokeAudioRef.current.src = original.url;
karaokeAudioRef.current.load();
karaokeAudioRef.current.play().catch(e => console.error(e));
}
}
};
const removeUserSong = (e: React.MouseEvent, songId: string) => {
e.stopPropagation();
setUserSongs(prev => prev.filter(s => s.id !== songId));
setCustomAudioMap(prev => {
if (prev[songId]) URL.revokeObjectURL(prev[songId]);
const newMap = { ...prev };
delete newMap[songId];
return newMap;
});
if (currentSong?.id === songId) {
setIsKaraokeActive(false);
setCurrentSong(null);
karaokeAudioRef.current?.pause();
}
};
const triggerUpload = (e: React.MouseEvent, song: any) => {
e.stopPropagation();
setSongToUpload(song);
fileInputRef.current?.click();
};
useEffect(() => {
let animationFrame: number;
const canvas = canvasRef.current;
const video = videoRef.current;
if (!canvas || !video) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const render = () => {
if (isCameraActive) {
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (video.readyState >= 2) {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
}
if (isSimActive) {
activeViewers.forEach((viewer, i) => {
const vImg = getCachedImage(viewer.img);
if (vImg?.complete) {
const x = 20 + (i % 4) * 110; const y = 100 + Math.floor(i / 4) * 110; const size = 100;
ctx.save(); ctx.strokeStyle = '#E3B341'; ctx.lineWidth = 2; ctx.strokeRect(x, y, size, size);
ctx.drawImage(vImg, x, y, size, size);
ctx.fillStyle = 'rgba(0,0,0,0.8)'; ctx.fillRect(x, y + size - 20, size, 20);
ctx.fillStyle = 'white'; ctx.font = 'bold 12px sans-serif'; ctx.fillText(viewer.name, x + 5, y + size - 5);
ctx.restore();
}
});
if (featuredViewer) {
const fImg = getCachedImage(featuredViewer.img);
if (fImg?.complete) {
const x = canvas.width - 320; const y = 100; const w = 300; const h = 300;
ctx.save(); ctx.strokeStyle = '#00F2FF'; ctx.lineWidth = 5; ctx.strokeRect(x, y, w, h);
ctx.drawImage(fImg, x, y, w, h);
ctx.fillStyle = 'rgba(0,0,0,0.8)'; ctx.fillRect(x, y + h - 40, w, 40);
ctx.fillStyle = '#00F2FF'; ctx.font = 'bold 18px sans-serif'; ctx.fillText(`DUETO COM: ${featuredViewer.name}`, x + 10, y + h - 12);
ctx.restore();
}
}
if ((isKaraokeActive && currentSong) || audioStatus === 'SYNCING') {
ctx.save();
ctx.fillStyle = 'rgba(0,0,0,0.85)';
ctx.fillRect(canvas.width/2 - 500, canvas.height - 250, 1000, 180);
ctx.strokeStyle = audioStatus === 'ERROR' ? '#FF0000' : audioStatus === 'SYNCING' ? '#00F2FF' : '#E3B341'; ctx.lineWidth = 4;
ctx.strokeRect(canvas.width/2 - 500, canvas.height - 250, 1000, 180);
ctx.fillStyle = audioStatus === 'SYNCING' ? '#00F2FF' : '#E3B341'; ctx.font = 'bold 20px monospace'; ctx.textAlign = 'center';
ctx.fillText(audioStatus === 'SYNCING' ? "🔄 ATIVANDO TODOS OS ÁUDIOS DO DISPOSITIVO" : `🎤 ${currentSong?.title?.toUpperCase()} - ${currentSong?.artist?.toUpperCase()} (${currentSong?.genre})`, canvas.width/2, canvas.height - 210);
if (audioStatus === 'ERROR') {
ctx.fillStyle = '#FF5555'; ctx.font = 'bold 32px sans-serif';
ctx.fillText(audioErrorMsg || "ERRO CRÍTICO NO ÁUDIO", canvas.width/2, canvas.height - 140);
} else {
ctx.fillStyle = 'white'; ctx.font = 'bold 48px sans-serif';
ctx.fillText(currentLyrics || "SOLTA O SOM!", canvas.width/2, canvas.height - 140);
}
ctx.fillStyle = 'rgba(255,255,255,0.1)'; ctx.fillRect(canvas.width/2 - 400, canvas.height - 100, 800, 10);
const progress = audioStatus === 'SYNCING' ? syncProgress / 100 : (playbackProgress / (karaokeAudioRef.current?.duration || 1));
ctx.fillStyle = audioStatus === 'SYNCING' ? '#00F2FF' : audioStatus === 'ACTIVE' ? '#E3B341' : '#666666'; ctx.fillRect(canvas.width/2 - 400, canvas.height - 100, progress * 800, 10);
ctx.restore();
}
ctx.fillStyle = 'rgba(255,0,0,0.8)'; ctx.font = 'bold 20px monospace';
ctx.fillText(`🔴 AO VIVO: ${audienceCount.toLocaleString()} PESSOAS`, 30, 50);
}
}
animationFrame = requestAnimationFrame(render);
};
render();
return () => cancelAnimationFrame(animationFrame);
}, [isCameraActive, isSimActive, activeViewers, featuredViewer, isKaraokeActive, currentLyrics, currentSong, getCachedImage, audienceCount, playbackProgress, audioStatus, audioErrorMsg, syncProgress]);
useEffect(() => {
let interval: any;
if (isSimActive) {
if (audioCtxRef.current && audioDestRef.current && !crowdAudioRef.current) {
const audio = new Audio(CROWD_SOUND_URL);
audio.loop = true; audio.volume = 0.2;
const source = audioCtxRef.current.createMediaElementSource(audio);
source.connect(audioDestRef.current);
source.connect(audioCtxRef.current.destination);
audio.play().catch(e => console.error(e));
crowdAudioRef.current = audio;
}
interval = setInterval(() => {
setAudienceCount(prev => Math.min(prev + Math.floor(Math.random() * 5000) + 1000, 1500000));
if (Math.random() > 0.8) {
const r = REAL_PEOPLE_AVATARS[Math.floor(Math.random() * REAL_PEOPLE_AVATARS.length)];
setActiveViewers(prev => [...prev.slice(-11), { id: Date.now(), ...r }]);
}
}, 2000);
} else {
setAudienceCount(0); setActiveViewers([]);
if (crowdAudioRef.current) { crowdAudioRef.current.pause(); crowdAudioRef.current = null; }
}
return () => clearInterval(interval);
}, [isSimActive]);
return (
<div className="relative h-screen w-full bg-[#1a1a1a] overflow-hidden flex flex-col font-sans">
<Head><title>KARAOKE GLOBAL 10K+ | AO VIVO</title></Head>
<input type="file" ref={fileInputRef} onChange={handleFileChange} accept="audio/*" className="hidden" />
<div className="absolute inset-0 z-0 bg-black overflow-hidden">
{!isCameraActive && (
<div className="flex flex-col items-center justify-center h-full space-y-8 z-10 relative">
<div className="w-40 h-40 border-4 border-[#E3B341] rounded-full flex items-center justify-center animate-bounce shadow-[0_0_50px_rgba(227,179,65,0.4)]">
<BaseIcon path={mdiMicrophone} size={80} className="text-[#E3B341]" />
</div>
<h1 className="text-white text-5xl font-black tracking-tighter uppercase italic text-center">
Mega <span className="text-[#E3B341]">Karaoke</span> 10.000+
<br /><span className="text-xl font-light tracking-widest text-white/60">BRASIL & MUNDO</span>
</h1>
<p className="text-white/80 font-bold uppercase tracking-widest max-w-lg text-center text-xs">
NUCLEAR AUDIO ENGINE ACTIVE - SINCRONIZAÇÃO TOTAL COM O NAVEGADOR
</p>
<button onClick={startCamera} className="bg-[#E3B341] text-black px-16 py-6 rounded-full font-black text-2xl uppercase tracking-tighter hover:scale-110 transition-all shadow-2xl">Entrar no Palco</button>
</div>
)}
<canvas ref={canvasRef} width={1920} height={1080} className="hidden" />
<video ref={videoRef} autoPlay playsInline muted className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-1000 ${isCameraActive ? 'opacity-100' : 'opacity-0'}`} />
{isSimActive && isInterfaceVisible && (
<div className="absolute top-24 left-6 z-40 grid grid-cols-4 gap-4 pointer-events-auto max-w-md animate-fade-in">
{activeViewers.map((viewer) => (
<button key={viewer.id} onClick={() => setFeaturedViewer(viewer)} className={`group relative w-24 h-24 rounded-2xl border-2 overflow-hidden transition-all hover:scale-110 ${featuredViewer?.id === viewer.id ? 'border-[#00F2FF] ring-4 ring-[#00F2FF]/20' : 'border-white/20 hover:border-[#E3B341]'}`}>
<Image src={viewer.img} alt={viewer.name} width={96} height={96} crossOrigin="anonymous" className="w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all" />
<div className="absolute bottom-0 w-full p-1 bg-black/60 text-[8px] font-bold text-white text-center">{viewer.name}</div>
</button>
))}
</div>
)}
{featuredViewer && isInterfaceVisible && (
<div className="absolute top-24 right-6 z-40 w-72 bg-black/90 border-4 border-[#00F2FF] rounded-3xl overflow-hidden shadow-2xl pointer-events-auto animate-fade-in">
<div className="relative">
<Image src={featuredViewer.img} alt={`Featured: ${featuredViewer.name}`} width={288} height={288} crossOrigin="anonymous" className="w-full h-72 object-cover" />
<div className="absolute top-4 left-4 flex items-center space-x-2 bg-red-600 px-3 py-1 rounded-full text-[10px] font-black text-white">
<div className="w-2 h-2 bg-white rounded-full animate-pulse"></div>
<span>DUETO AO VIVO</span>
</div>
<button onClick={() => setFeaturedViewer(null)} className="absolute top-4 right-4 bg-black/60 p-1 rounded-full text-white"><BaseIcon path={mdiClose} size={20} /></button>
</div>
<div className="p-4 bg-white/5 border-t border-white/10 text-center">
<div className="text-[#00F2FF] text-[10px] font-black uppercase">Participante Selecionado</div>
<div className="text-white text-lg font-bold">{featuredViewer.name}</div>
</div>
</div>
)}
{isKaraokeActive && currentSong && isInterfaceVisible && (
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 z-50 w-full max-w-4xl px-8 pointer-events-none animate-slide-up">
<div className="bg-black/90 border-t-8 border-[#E3B341] p-10 rounded-3xl backdrop-blur-3xl shadow-[0_-20px_100px_rgba(0,0,0,0.8)] text-center pointer-events-auto">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<BaseIcon path={customAudioMap[currentSong.id] ? mdiCheckCircle : mdiMusicNote} size={24} className={customAudioMap[currentSong.id] ? "text-green-500" : "text-[#E3B341]"} />
<span className="text-[#E3B341] text-lg font-black uppercase tracking-widest">{currentSong.title} - {currentSong.artist}</span>
</div>
<div className="flex items-center space-x-2">
<div className={`w-3 h-3 rounded-full animate-pulse ${audioStatus === 'ACTIVE' ? 'bg-green-500' : audioStatus === 'LOADING' ? 'bg-yellow-500' : audioStatus === 'SYNCING' ? 'bg-blue-500' : 'bg-red-500'}`}></div>
<span className="text-[10px] text-white/60 font-black uppercase">{audioStatus}</span>
</div>
</div>
<div className={`text-5xl font-black leading-tight drop-shadow-[0_4px_4px_rgba(0,0,0,1)] transition-all duration-300 ${audioStatus === 'ERROR' ? 'text-red-500' : 'text-white'}`}>
{audioStatus === 'ERROR' ? audioErrorMsg : (currentLyrics || "VAI COMEÇAR...")}
</div>
<div className="mt-8 h-2 bg-white/10 rounded-full overflow-hidden relative">
<div className="h-full bg-gradient-to-r from-[#E3B341] to-[#ffda85] shadow-[0_0_15px_rgba(227,179,65,0.6)]" style={{ width: `${(playbackProgress / (karaokeAudioRef.current?.duration || 1)) * 100}%` }}></div>
</div>
<div className="mt-8 flex justify-center space-x-6 items-center">
<button onClick={togglePlayback} className="bg-[#E3B341] text-black w-20 h-20 flex items-center justify-center rounded-full hover:scale-110 transition-all shadow-[0_0_30px_rgba(227,179,65,0.4)]">
<BaseIcon path={isPaused ? mdiPlay : mdiPause} size={48} />
</button>
<div className="flex flex-col items-center space-y-2">
<button onClick={() => setIsInterfaceVisible(false)} className="bg-white/10 text-white p-4 rounded-full hover:bg-white/20 transition-all">
<BaseIcon path={mdiEyeOff} size={24} />
</button>
<span className="text-[8px] text-white/40 font-black">OCULTAR</span>
</div>
<button onClick={() => selectSong(currentSong)} className={`p-4 rounded-full transition-all ${audioStatus === 'ERROR' ? 'bg-red-600 text-white animate-bounce shadow-lg' : 'bg-white/10 text-white hover:bg-white/20'}`}>
<BaseIcon path={mdiRefresh} size={24} />
</button>
</div>
</div>
</div>
)}
</div>
{isCameraActive && (
<>
{!isInterfaceVisible && (
<button onClick={() => setIsInterfaceVisible(true)} className="absolute bottom-10 right-10 z-[60] bg-[#E3B341] text-black p-6 rounded-full shadow-2xl hover:scale-110 transition-all animate-bounce">
<BaseIcon path={mdiEye} size={40} />
</button>
)}
{isInterfaceVisible && (
<div className="absolute inset-0 z-30 p-8 flex flex-col justify-between pointer-events-none animate-fade-in">
<div className="flex justify-between items-start pointer-events-auto">
<div className="flex flex-col space-y-4">
<div className="bg-black/80 p-6 rounded-[2rem] border border-white/10 backdrop-blur-xl shadow-2xl flex items-center space-x-6">
<div className="flex flex-col items-center">
<button onClick={() => setIsMicOn(!isMicOn)} className={`p-4 rounded-full transition-all ${isMicOn ? 'bg-green-600 shadow-[0_0_20px_rgba(34,197,94,0.4)]' : 'bg-red-600'}`}>
<BaseIcon path={isMicOn ? mdiMicrophone : mdiMicrophoneOff} size={32} className="text-white" />
</button>
<span className="text-[10px] font-black text-white mt-2">MIC</span>
</div>
<div className="flex flex-col items-center">
<button onClick={() => setIsMonitorOn(!isMonitorOn)} className={`p-4 rounded-full transition-all ${isMonitorOn ? 'bg-[#00F2FF] text-black shadow-[0_0_20px_rgba(0,242,255,0.4)]' : 'bg-white/10 text-white'}`}>
<BaseIcon path={mdiHeadphones} size={32} />
</button>
<span className="text-[10px] font-black text-white mt-2">RETORNO</span>
</div>
<div className="w-px h-12 bg-white/20"></div>
<div className="flex flex-col space-y-2 min-w-[120px]">
<div className="flex justify-between text-[10px] font-black text-[#E3B341]"><span>EFEITO</span><span>{(reverbLevel * 100).toFixed(0)}%</span></div>
<input type="range" min="0" max="1" step="0.1" value={reverbLevel} onChange={(e) => setReverbLevel(parseFloat(e.target.value))} className="w-full h-1 bg-white/10 accent-[#E3B341] rounded-lg appearance-none cursor-pointer" />
</div>
<div className="flex flex-col space-y-2 min-w-[120px]">
<div className="flex justify-between text-[10px] font-black text-[#00F2FF]"><span>VOLUME</span><span>{(volumeLevel * 100).toFixed(0)}%</span></div>
<input type="range" min="0" max="1" step="0.1" value={volumeLevel} onChange={(e) => setVolumeLevel(parseFloat(e.target.value))} className="w-full h-1 bg-white/10 accent-[#00F2FF] rounded-lg appearance-none cursor-pointer" />
</div>
<div className="flex flex-col items-center">
<button onClick={syncAll} className={`p-4 rounded-full transition-all ${audioStatus === 'SYNCING' ? 'bg-[#00F2FF] animate-spin text-black' : audioStatus === 'ERROR' ? 'bg-red-600 animate-pulse text-white' : 'bg-white/5 text-[#E3B341] hover:bg-white/10'}`}>
<BaseIcon path={audioStatus === 'SYNCING' ? mdiSync : audioStatus === 'ERROR' ? mdiVolumeVariantOff : mdiTune} size={32} />
</button>
<span className={`text-[10px] font-black mt-2 text-center uppercase ${audioStatus === 'ERROR' ? 'text-red-500' : 'text-white'}`}>
REATIVAR<br/>ÁUDIO
</span>
</div>
</div>
<div className="bg-red-600 text-white px-6 py-3 rounded-full font-black text-sm shadow-2xl flex items-center space-x-3 w-fit">
<div className="w-3 h-3 bg-white rounded-full animate-pulse"></div>
<span>{audienceCount.toLocaleString()} PESSOAS AO VIVO</span>
</div>
</div>
<div className="flex space-x-4">
<button onClick={() => setIsInterfaceVisible(false)} className="p-6 bg-black/80 border-2 border-white/20 rounded-full text-white hover:border-[#E3B341] transition-all shadow-2xl"><BaseIcon path={mdiEyeOff} size={40} /></button>
<button onClick={() => setShowPlaylist(!showPlaylist)} className={`p-6 bg-black/80 border-2 rounded-full transition-all shadow-2xl ${showPlaylist ? 'border-[#E3B341] text-[#E3B341]' : 'border-white/20 text-white hover:border-[#E3B341]'}`}><BaseIcon path={mdiMusicNote} size={40} /></button>
<button onClick={isRecording ? () => { mediaRecorder?.stop(); setIsRecording(false); } : () => {
if (!canvasRef.current || !audioDestRef.current) return;
const combinedStream = new MediaStream([
...canvasRef.current.captureStream(60).getVideoTracks(),
...audioDestRef.current.stream.getAudioTracks()
]);
const recorder = new MediaRecorder(combinedStream, { mimeType: 'video/webm;codecs=vp9,opus', bitsPerSecond: 10000000 });
const chunks: Blob[] = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = () => setVideoUrl(URL.createObjectURL(new Blob(chunks, { type: 'video/webm' })));
recorder.start(); setIsRecording(true); setMediaRecorder(recorder);
}} className={`p-6 bg-black/80 border-2 rounded-full transition-all shadow-2xl ${isRecording ? 'border-red-600 text-red-600 scale-110' : 'border-white/20 text-white hover:border-red-600'}`}><BaseIcon path={isRecording ? mdiStop : mdiRecord} size={40} /></button>
{videoUrl && !isRecording && <button onClick={() => { const a = document.createElement('a'); a.href = videoUrl; a.download = `meu-show-global.webm`; a.click(); }} className="p-6 bg-green-600 text-white rounded-full shadow-2xl animate-bounce"><BaseIcon path={mdiDownload} size={40} /></button>}
<button onClick={() => window.location.reload()} className="p-6 bg-black/80 border-2 border-white/20 rounded-full text-white hover:bg-red-600 transition-all"><BaseIcon path={mdiClose} size={40} /></button>
</div>
</div>
{showPlaylist && (
<div className="absolute top-8 right-8 w-[35rem] bg-black/95 border-4 border-[#E3B341] p-8 rounded-[3rem] shadow-[0_0_100px_rgba(0,0,0,0.9)] pointer-events-auto z-50 animate-slide-up flex flex-col h-[90vh]">
<div className="flex items-center justify-between mb-6">
<div className="flex flex-col">
<h3 className="text-[#E3B341] font-black text-2xl italic uppercase tracking-tighter">Estúdio 10.000+</h3>
<div className="flex items-center space-x-2">
<div className={`w-2 h-2 rounded-full ${audioCtxRef.current?.state === 'running' ? 'bg-green-500' : 'bg-red-500'}`}></div>
<p className="text-[10px] text-white/40 font-bold uppercase tracking-widest">NAVIGATOR SYNC: {audioCtxRef.current?.state === 'running' ? 'ATIVO' : 'AGUARDANDO'}</p>
</div>
</div>
<button onClick={() => setShowPlaylist(false)} className="bg-white/5 p-2 rounded-full text-white/40 hover:text-white transition-all"><BaseIcon path={mdiClose} size={28} /></button>
</div>
<div className="relative mb-6">
<div className="absolute inset-y-0 left-5 flex items-center pointer-events-none">
<BaseIcon path={mdiMagnify} size={24} className="text-white/30" />
</div>
<input type="text" placeholder="BUSCAR E CONFIGURAR..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="w-full bg-white/5 border-2 border-white/10 rounded-2xl py-5 pl-14 pr-6 text-white font-bold focus:outline-none focus:border-[#E3B341] transition-all placeholder:text-white/20 uppercase text-sm" />
</div>
<div className="flex space-x-2 overflow-x-auto pb-4 mb-4 custom-scrollbar no-scrollbar">
{GENRES.map(genre => (
<button key={genre.name} onClick={() => setSelectedGenre(genre.name)} className={`flex items-center space-x-2 px-6 py-3 rounded-full whitespace-nowrap font-black text-[10px] uppercase transition-all border-2 ${selectedGenre === genre.name ? 'bg-[#E3B341] border-[#E3B341] text-black shadow-[0_0_20px_rgba(227,179,65,0.3)]' : 'bg-white/5 border-white/10 text-white/60 hover:border-white/30'}`}>
<BaseIcon path={genre.icon} size={16} />
<span>{genre.name}</span>
</button>
))}
</div>
<div className="flex-grow overflow-y-auto pr-2 space-y-3 custom-scrollbar">
<div className="p-6 border-2 border-dashed border-[#E3B341]/40 rounded-[2rem] text-center hover:bg-white/5 transition-all cursor-pointer group mb-6" onClick={() => {
setSongToUpload({ id: 'NEW_CUSTOM' });
fileInputRef.current?.click();
}}>
<BaseIcon path={mdiUpload} size={40} className="text-[#E3B341] mx-auto mb-3" />
<h4 className="text-white font-black uppercase text-sm italic tracking-widest">Adicionar Nova Música do Dispositivo</h4>
<p className="text-[9px] text-white/40 font-bold mt-1">SINCROZINA AUTOMATICAMENTE APÓS O CARREGAMENTO</p>
</div>
{filteredSongs.length > 0 ? filteredSongs.map(song => (
<div key={song.id} className="relative">
<div className={`w-full flex items-center justify-between p-4 rounded-2xl border-2 transition-all group cursor-pointer ${currentSong?.id === song.id ? 'bg-[#E3B341] border-[#E3B341] text-black' : 'bg-white/5 border-white/10 text-white hover:border-[#E3B341] hover:bg-white/10'}`} onClick={() => selectSong(song)}>
<div className="flex flex-col items-start overflow-hidden flex-grow">
<span className="font-black text-lg truncate w-full italic leading-tight flex items-center space-x-2">
{customAudioMap[song.id] && <BaseIcon path={mdiCheckCircle} size={16} className="text-green-500 animate-pulse" />}
<span>{song.title}</span>
</span>
<div className="flex items-center space-x-2 mt-1">
<span className="text-[10px] font-black uppercase opacity-60 tracking-wider">{song.artist}</span>
<span className="w-1 h-1 bg-white/30 rounded-full"></span>
<span className="text-[9px] font-black uppercase text-[#E3B341] group-hover:text-inherit">{song.genre}</span>
</div>
</div>
<div className="flex items-center space-x-2">
<button onClick={(e) => { e.stopPropagation(); setEditingSongId(editingSongId === song.id ? null : song.id); }} className={`p-2 rounded-full transition-all ${editingSongId === song.id ? 'bg-black text-[#E3B341]' : 'hover:bg-white/10'}`}>
<BaseIcon path={mdiCog} size={20} />
</button>
<div className={`p-3 rounded-full transition-all ${currentSong?.id === song.id ? 'bg-black text-[#E3B341]' : 'bg-white/10 text-white group-hover:bg-[#E3B341] group-hover:text-black'}`}>
<BaseIcon path={currentSong?.id === song.id ? (isPaused ? mdiPlay : mdiPause) : mdiPlay} size={24} />
</div>
</div>
</div>
{editingSongId === song.id && (
<div className="mt-2 p-4 bg-white/5 rounded-2xl border border-white/10 flex items-center justify-around animate-fade-in pointer-events-auto">
<button onClick={(e) => triggerUpload(e, song)} className="flex flex-col items-center space-y-1 group">
<div className="p-3 bg-[#E3B341] text-black rounded-full group-hover:scale-110 transition-all shadow-lg"><BaseIcon path={mdiUpload} size={20} /></div>
<span className="text-[8px] font-black text-white/60">TROCAR ÁUDIO</span>
</button>
{customAudioMap[song.id] && !song.isUserAdded && (
<button onClick={(e) => resetToOriginal(e, song.id)} className="flex flex-col items-center space-y-1 group">
<div className="p-3 bg-blue-600 text-white rounded-full group-hover:scale-110 transition-all shadow-lg"><BaseIcon path={mdiRefresh} size={20} /></div>
<span className="text-[8px] font-black text-white/60">RESTAURAR</span>
</button>
)}
{song.isUserAdded && (
<button onClick={(e) => removeUserSong(e, song.id)} className="flex flex-col items-center space-y-1 group">
<div className="p-3 bg-red-600 text-white rounded-full group-hover:scale-110 transition-all shadow-lg"><BaseIcon path={mdiTrashCan} size={20} /></div>
<span className="text-[8px] font-black text-white/60">REMOVER</span>
</button>
)}
<button onClick={() => setEditingSongId(null)} className="flex flex-col items-center space-y-1 group">
<div className="p-3 bg-white/10 text-white rounded-full group-hover:scale-110 transition-all"><BaseIcon path={mdiClose} size={20} /></div>
<span className="text-[8px] font-black text-white/60">FECHAR</span>
</button>
</div>
)}
</div>
)) : (
<div className="flex flex-col items-center justify-center py-20 text-white/20">
<BaseIcon path={mdiFilterVariant} size={64} className="mb-4" />
<p className="font-black uppercase tracking-widest text-sm">Nenhuma música encontrada</p>
</div>
)}
</div>
<div className="mt-6 pt-6 border-t border-white/10 flex items-center justify-between">
<div className="flex flex-col">
<span className="text-[9px] font-black text-white/30 uppercase tracking-widest">Estúdio Studio Master 1.0</span>
<span className="text-[#E3B341] text-xs font-black">PLAYBACKS: {fullSongDatabase.length} DISPONÍVEIS</span>
</div>
<div className="flex items-center space-x-2">
<button onClick={syncAll} className="flex items-center space-x-2 bg-[#E3B341]/10 text-[#E3B341] px-4 py-2 rounded-full border border-[#E3B341]/20 hover:bg-[#E3B341]/20 transition-all">
<BaseIcon path={mdiSync} size={16} className={audioStatus === 'SYNCING' ? 'animate-spin' : ''} />
<span className="text-[10px] font-black uppercase">SINCRONIZAR TUDO</span>
</button>
</div>
</div>
</div>
)}
</div>
)}
</>
)}
<style jsx global>{`
@keyframes slide-up { from { opacity: 0; transform: translateY(40px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
.animate-slide-up { animation: slide-up 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
.animate-fade-in { animation: fade-in 0.5s ease-out forwards; }
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
.custom-scrollbar::-webkit-scrollbar-track { background: rgba(255,255,255,0.02); border-radius: 10px; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #E3B341; border-radius: 10px; }
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
`}</style>
</div>
);
};
ObservationPage.getLayout = function getLayout(page: ReactElement) { return <LayoutAuthenticated>{page}</LayoutAuthenticated>; };
export default ObservationPage;