This commit is contained in:
Flatlogic Bot 2026-02-26 20:39:38 +00:00
parent c8e1ff52ac
commit 8e65ab42ed

View File

@ -27,13 +27,16 @@ import {
mdiEyeOff,
mdiUpload,
mdiCheckCircle,
mdiCog,
mdiRefresh,
mdiTrashCan,
} from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
import { useAppDispatch } from '../stores/hooks';
import LayoutAuthenticated from '../layouts/Authenticated';
// Extensive song database simulation
const SONG_DATABASE = [
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' },
@ -77,11 +80,11 @@ const SONG_DATABASE = [
{ 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' },
];
// Generate more to reach a "feeling" of 10,000
for (let i = 1; i <= 500; i++) {
// 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)];
SONG_DATABASE.push({
INITIAL_SONG_DATABASE.push({
id: `ext-${i}`,
genre: genre,
title: `${genre} Classic Vol ${i}`,
@ -151,11 +154,19 @@ const ObservationPage = () => {
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 imageCache = useRef<Map<string, HTMLImageElement>>(new Map());
// Combined song database
const fullSongDatabase = useMemo(() => {
return [...userSongs, ...INITIAL_SONG_DATABASE];
}, [userSongs]);
// Persistent karaoke audio setup
useEffect(() => {
const audio = new Audio();
@ -197,13 +208,13 @@ const ObservationPage = () => {
}, []);
const filteredSongs = useMemo(() => {
return SONG_DATABASE.filter(song => {
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;
});
}, [searchQuery, selectedGenre]);
}, [fullSongDatabase, searchQuery, selectedGenre]);
const initAudioContext = async (stream: MediaStream) => {
if (!audioCtxRef.current) {
@ -302,7 +313,7 @@ const ObservationPage = () => {
setShowPlaylist(false);
audio.pause();
// Use custom audio if available, otherwise use default URL
// Prioritize user blob URL for this specific song ID
audio.src = customAudioMap[song.id] || song.url;
audio.load();
@ -331,28 +342,73 @@ const ObservationPage = () => {
const file = e.target.files?.[0];
if (file && songToUpload) {
const url = URL.createObjectURL(file);
setCustomAudioMap(prev => {
// Revoke previous custom audio for this song if it exists
if (prev[songToUpload.id]) {
URL.revokeObjectURL(prev[songToUpload.id]);
}
return { ...prev, [songToUpload.id]: url };
});
setSongToUpload(null);
// If we're currently playing this song, update the source immediately
if (currentSong?.id === songToUpload.id && karaokeAudioRef.current) {
const wasPaused = karaokeAudioRef.current.paused;
karaokeAudioRef.current.src = url;
karaokeAudioRef.current.load();
if (!wasPaused) {
karaokeAudioRef.current.play().catch(err => console.error("Playback update error:", err));
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 };
});
// Immediate switch if playing
if (currentSong?.id === songToUpload.id && karaokeAudioRef.current) {
const wasPaused = karaokeAudioRef.current.paused;
karaokeAudioRef.current.src = url;
karaokeAudioRef.current.load();
if (!wasPaused) karaokeAudioRef.current.play().catch(e => console.error(e));
}
}
setSongToUpload(null);
}
// Reset file input value to allow selecting the same file again if needed
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;
});
// Restore if playing
if (currentSong?.id === songId && karaokeAudioRef.current) {
const original = INITIAL_SONG_DATABASE.find(s => s.id === songId);
if (original) {
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);
@ -469,6 +525,9 @@ const ObservationPage = () => {
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">
Configure cada música com seu áudio local ou use nossa biblioteca cloud de alta fidelidade
</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>
)}
@ -510,7 +569,7 @@ const ObservationPage = () => {
<div className="flex items-center justify-center space-x-3 mb-6">
<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>
{customAudioMap[currentSong.id] && <span className="text-[10px] bg-green-500 text-black px-2 py-0.5 rounded font-black">LOCAL</span>}
{customAudioMap[currentSong.id] && <span className="text-[10px] bg-green-500 text-black px-2 py-0.5 rounded font-black">ÁUDIO LOCAL ATIVO</span>}
</div>
<div className="text-white text-5xl font-black leading-tight drop-shadow-[0_4px_4px_rgba(0,0,0,1)] transition-all duration-300">
{currentLyrics || "VAI COMEÇAR..."}
@ -590,11 +649,11 @@ const ObservationPage = () => {
</div>
{showPlaylist && (
<div className="absolute top-8 right-8 w-[32rem] 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="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>
<p className="text-[10px] text-white/40 font-bold uppercase tracking-widest">Configure seus playbacks e brilhe</p>
<p className="text-[10px] text-white/40 font-bold uppercase tracking-widest">Configuração Avançada de Playbacks</p>
</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>
@ -603,7 +662,7 @@ const ObservationPage = () => {
<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 ENTRE 10.000 MÚSICAS..." 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" />
<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">
@ -616,56 +675,87 @@ const ObservationPage = () => {
</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">CARREGUE SEUS PRÓPRIOS PLAYBACKS MP3/WAV</p>
</div>
{filteredSongs.length > 0 ? filteredSongs.map(song => (
<div key={song.id} 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" />}
<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) => triggerUpload(e, song)} title="Trocar áudio por arquivo local" className={`p-2 rounded-full transition-all ${currentSong?.id === song.id ? 'bg-black/20 text-black hover:bg-black/40' : 'bg-white/5 text-[#E3B341] hover:bg-white/20'}`}>
<BaseIcon path={mdiUpload} 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 ? mdiPause : mdiPlay} size={24} />
</div>
</div>
<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" />}
<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 ? 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>
<p className="text-[10px] font-bold mt-2">TENTE OUTROS TERMOS OU CATEGORIAS</p>
</div>
)}
<div className="p-8 border-2 border-dashed border-white/10 rounded-[2rem] text-center opacity-40 hover:opacity-100 transition-all cursor-pointer group" onClick={() => {
setSongToUpload({ id: 'new-custom', title: 'Upload Personalizado', artist: 'Arquivo Local', genre: 'Personalizado' });
fileInputRef.current?.click();
}}>
<BaseIcon path={mdiCreation} size={48} className="text-[#E3B341] mx-auto mb-4 group-hover:scale-125 transition-all" />
<h4 className="text-white font-black uppercase tracking-tighter">Carregar Áudio Personalizado</h4>
<p className="text-[10px] text-white/60 font-bold mt-1 uppercase">Substitua playbacks por seus próprios arquivos MP3/WAV</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">Configuração de Playback</span>
<span className="text-[#E3B341] text-xs font-black">LOCAL & CLOUD SYNC ATIVO</span>
<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-4">
<BaseIcon path={mdiVolumeHigh} size={20} className="text-white/40" />
<div className="w-24 h-1 bg-white/10 rounded-full overflow-hidden">
<div className="w-3/4 h-full bg-[#E3B341]"></div>
</div>
<div className="flex items-center space-x-2">
{Object.keys(customAudioMap).length > 0 && (
<span className="text-[10px] font-black text-green-500 bg-green-500/10 px-3 py-1 rounded-full border border-green-500/20">
{Object.keys(customAudioMap).length} ÁUDIOS PERSONALIZADOS
</span>
)}
</div>
</div>
</div>