teste upload de sample com mic
Deploy / Deploy (push) Successful in 1m28s Details

This commit is contained in:
JotaChina 2025-12-08 14:26:02 -03:00
parent 6bd74b65fe
commit 0768b85700
2 changed files with 221 additions and 88 deletions

View File

@ -2377,6 +2377,13 @@
"_isFile": true
}
},
"Teste": {
"Testes": {
"bassdrum_acoustic01_-_Copia.ogg": {
"_isFile": true
}
}
},
"misc": {
"applause01.ogg": {
"_isFile": true

View File

@ -179,24 +179,65 @@ permalink: /samples/
</header>
<section class="modal-card-body">
<div class="tabs is-toggle is-fullwidth is-small mb-4">
<ul>
<li class="is-active" id="tab-file-trigger">
<a>
<span class="icon is-small"><i class="fa-solid fa-file-arrow-up"></i></span>
<span>Arquivo</span>
</a>
</li>
<li id="tab-mic-trigger">
<a>
<span class="icon is-small"><i class="fa-solid fa-microphone"></i></span>
<span>Microfone</span>
</a>
</li>
</ul>
</div>
<form id="sample-upload-form">
<div class="field">
<label class="label">Arquivo de Áudio</label>
<div class="control">
<div class="file has-name is-fullwidth">
<label class="file-label">
<input class="file-input" type="file" name="sample_file" accept=".wav,.mp3,.ogg,.flac" required>
<span class="file-cta">
<span class="file-icon"><i class="fa-solid fa-upload"></i></span>
<span class="file-label">Escolher arquivo...</span>
</span>
<span class="file-name" id="upload-filename-display">Nenhum arquivo selecionado</span>
</label>
<div id="section-file-upload">
<div class="field">
<label class="label">Arquivo de Áudio</label>
<div class="control">
<div class="file has-name is-fullwidth">
<label class="file-label">
<input class="file-input" type="file" name="sample_file" accept=".wav,.mp3,.ogg,.flac" id="file-input-real">
<span class="file-cta">
<span class="file-icon"><i class="fa-solid fa-upload"></i></span>
<span class="file-label">Escolher arquivo...</span>
</span>
<span class="file-name" id="upload-filename-display">Nenhum selecionado</span>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="field">
<div id="section-mic-record" class="is-hidden has-text-centered mb-4 p-4" style="background-color: #f5f5f5; border-radius: 8px;">
<p class="mb-3 is-size-7 has-text-grey">Clique para começar a gravar</p>
<button type="button" id="record-btn" class="button is-rounded is-large mb-3" style="width: 80px; height: 80px; border: 4px solid #ddd; transition: all 0.2s;">
<span class="icon is-large has-text-danger"><i class="fa-solid fa-microphone"></i></span>
</button>
<div id="recording-indicator" class="is-hidden">
<span class="tag is-danger is-light mb-2 pulse-animation">🔴 Gravando...</span>
</div>
<audio id="record-preview" controls class="is-hidden mt-3" style="width: 100%;"></audio>
<div class="field mt-3">
<label class="label is-small has-text-left">Nome da Gravação</label>
<div class="control">
<input class="input is-small" type="text" id="record-filename" placeholder="Ex: minha_voz_01">
</div>
</div>
</div>
<div class="field mt-4">
<label class="label">Salvar na Pasta</label>
<div class="control has-icons-left">
<input class="input" type="text" name="subfolder" placeholder="Ex: Drums/Kicks (Vazio = Raiz)" id="upload-subfolder">
@ -233,6 +274,15 @@ permalink: /samples/
.project-card:hover { transform: translateY(-5px); box-shadow: 0 8px 20px rgba(50, 115, 220, 0.15); border-color: #3273dc !important; background-color: #fff !important; }
.clickable-tag:hover { background-color: #e3effd !important; color: #3273dc !important; border-color: #3273dc !important; transform: scale(1.05); }
.tag.is-active-filter { background-color: #3273dc !important; color: #fff !important; border-color: #3273dc !important; font-weight: bold; box-shadow: 0 2px 5px rgba(50, 115, 220, 0.3); }
/* Animação para Gravação */
@keyframes pulse-red {
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 56, 96, 0.7); }
70% { transform: scale(1.05); box-shadow: 0 0 0 10px rgba(255, 56, 96, 0); }
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 56, 96, 0); }
}
.pulse-animation { animation: pulse-red 1.5s infinite; }
.is-recording { border-color: #ff3860 !important; background-color: #ffe0e6 !important; animation: pulse-red 1.5s infinite; }
</style>
<script>
@ -258,7 +308,6 @@ document.addEventListener('DOMContentLoaded', () => {
// === FUNÇÕES DE UTILIDADE ===
// Remove extensão do arquivo (ex: "kick.wav" -> "kick")
function removeExtension(filename) {
return filename.replace(/\.[^/.]+$/, "");
}
@ -376,65 +425,42 @@ document.addEventListener('DOMContentLoaded', () => {
}
filterDisplayName.textContent = sampleName;
// Preparação do Alvo (Target)
const targetClean = sampleName.trim().toLowerCase();
const targetBase = removeExtension(targetClean); // Ex: "kick" (sem .wav)
console.log(`🔍 Buscando por: "${sampleName}" (Base: "${targetBase}")`);
const targetBase = removeExtension(targetClean);
let foundCount = 0;
projects.forEach(project => {
const projectSamplesStr = project.getAttribute('data-samples');
if (!projectSamplesStr) {
project.style.display = 'none';
return;
}
if (!projectSamplesStr) { project.style.display = 'none'; return; }
const projectSamples = projectSamplesStr.split(',');
// Verifica se ALGUM sample do projeto bate com o alvo (com ou sem extensão)
const hasMatch = projectSamples.some(s => {
const samplePathClean = s.trim().toLowerCase();
const sampleFileName = samplePathClean.split(/[/\\]/).pop(); // Pega nome do arquivo
const sampleBaseName = removeExtension(sampleFileName); // Remove extensão
// 1. Tenta match exato de nome (ex: kick.wav == kick.wav)
if (sampleFileName === targetClean) return true;
const sampleFileName = samplePathClean.split(/[/\\]/).pop();
const sampleBaseName = removeExtension(sampleFileName);
// 2. Tenta match de nome base (ex: kick.wav == kick.ogg)
if (sampleFileName === targetClean) return true;
if (sampleBaseName === targetBase && sampleBaseName !== "") return true;
return false;
});
if (hasMatch) {
project.style.display = 'block';
foundCount++;
// Destaca a tag correta dentro do card
const tags = project.querySelectorAll('.sample-tag-item');
tags.forEach(tag => {
const tagValClean = tag.dataset.value.trim().toLowerCase().split(/[/\\]/).pop();
const tagBase = removeExtension(tagValClean);
if(tagBase === targetBase) tag.classList.add('is-active-filter');
else tag.classList.remove('is-active-filter');
});
} else {
project.style.display = 'none';
}
});
console.log(`✅ Encontrados ${foundCount} projetos.`);
// Rola até os resultados se encontrou algo
if(foundCount > 0) {
document.getElementById('project-list').scrollIntoView({ behavior: 'smooth' });
}
if(foundCount > 0) document.getElementById('project-list').scrollIntoView({ behavior: 'smooth' });
}
// Inicialização
@ -442,7 +468,6 @@ document.addEventListener('DOMContentLoaded', () => {
renderBreadcrumbs();
renderBrowser();
// Eventos para as tags nos cards
document.querySelectorAll('.sample-tag-item').forEach(tag => {
tag.addEventListener('click', function(e) {
e.preventDefault();
@ -451,16 +476,13 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
if(clearFilterButton) {
clearFilterButton.addEventListener('click', () => filterBySample(null));
}
if(clearFilterButton) clearFilterButton.addEventListener('click', () => filterBySample(null));
// Leitura URL
const urlParams = new URLSearchParams(window.location.search);
const sampleParam = urlParams.get('sample');
if (sampleParam) filterBySample(sampleParam);
// Modal
// Modal Preview
const modal = document.getElementById('preview-modal');
const iframe = document.getElementById('preview-iframe');
const modalTitle = document.getElementById('modal-title');
@ -488,38 +510,133 @@ document.addEventListener('DOMContentLoaded', () => {
openModal(btn.dataset.targetUrl, btn.dataset.modalTitle, btn.dataset.fullBtnText, btn.dataset.fullBtnLink);
});
});
iframe.addEventListener('load', () => { try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
const style = iframeDoc.createElement('style');
style.textContent = `.tabs, .navbar, .sidebar-wrapper, .main-header { display: none !important; } .publication { padding-top: 0 !important; } body { background-color: #fff !important; }`;
iframeDoc.head.appendChild(style);
} catch(e){} });
closeButtons.forEach(el => el.addEventListener('click', closeModal));
document.addEventListener('keydown', (e) => { if (e.key === "Escape") closeModal(); });
// === LÓGICA DE UPLOAD ===
// ==========================================================
// === LÓGICA DE UPLOAD & GRAVAÇÃO (MODIFICADA) ===
// ==========================================================
const uploadModal = document.getElementById('upload-sample-modal');
const btnOpenUpload = document.getElementById('btn-open-upload');
const btnCloseUpload = document.getElementById('close-upload-modal');
const btnCancelUpload = document.getElementById('cancel-upload');
const fileInput = document.querySelector('#sample-upload-form input[type="file"]');
const fileInput = document.getElementById('file-input-real');
const fileNameDisplay = document.getElementById('upload-filename-display');
const subfolderInput = document.getElementById('upload-subfolder');
const confirmUploadBtn = document.getElementById('confirm-upload-btn');
const uploadProgress = document.getElementById('upload-progress');
const uploadStatus = document.getElementById('upload-status');
// Abrir/Fechar Modal
// Elementos das Abas
const tabFile = document.getElementById('tab-file-trigger');
const tabMic = document.getElementById('tab-mic-trigger');
const sectionFile = document.getElementById('section-file-upload');
const sectionMic = document.getElementById('section-mic-record');
// Elementos de Gravação
const recordBtn = document.getElementById('record-btn');
const recordIndicator = document.getElementById('recording-indicator');
const recordPreview = document.getElementById('record-preview');
const recordNameInput = document.getElementById('record-filename');
let isRecording = false;
let mediaRecorder = null;
let audioChunks = [];
let audioBlob = null;
let activeTab = 'file'; // 'file' or 'mic'
// Alternância de Abas
function switchTab(mode) {
activeTab = mode;
if(mode === 'file') {
tabFile.classList.add('is-active');
tabMic.classList.remove('is-active');
sectionFile.classList.remove('is-hidden');
sectionMic.classList.add('is-hidden');
stopRecordingLogic(); // Garante que parou de gravar
} else {
tabMic.classList.add('is-active');
tabFile.classList.remove('is-active');
sectionMic.classList.remove('is-hidden');
sectionFile.classList.add('is-hidden');
// Sugere nome pro arquivo
if(!recordNameInput.value) recordNameInput.value = "gravacao_" + new Date().toLocaleTimeString().replace(/:/g, '-');
}
}
tabFile.onclick = () => switchTab('file');
tabMic.onclick = () => switchTab('mic');
// Lógica do Microfone
recordBtn.onclick = async () => {
if (!isRecording) {
// INICIAR GRAVAÇÃO
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.start();
isRecording = true;
// UI Updates
recordBtn.classList.add('is-recording');
recordIndicator.classList.remove('is-hidden');
recordPreview.classList.add('is-hidden');
uploadStatus.textContent = "Gravando... Clique no microfone para parar.";
uploadStatus.className = "help has-text-danger";
uploadStatus.classList.remove('is-hidden');
mediaRecorder.ondataavailable = event => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = () => {
audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); // WebM é padrão comum
const audioUrl = URL.createObjectURL(audioBlob);
recordPreview.src = audioUrl;
recordPreview.classList.remove('is-hidden');
// Cleanup streams
stream.getTracks().forEach(track => track.stop());
};
} catch (err) {
alert("Erro ao acessar microfone: " + err.message);
console.error(err);
}
} else {
// PARAR GRAVAÇÃO
stopRecordingLogic();
}
};
function stopRecordingLogic() {
if(mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
}
isRecording = false;
recordBtn.classList.remove('is-recording');
recordIndicator.classList.add('is-hidden');
uploadStatus.textContent = "Gravação finalizada. Pode enviar.";
uploadStatus.className = "help has-text-success";
}
// Modal Open/Close
function toggleUploadModal(show) {
if(show) {
uploadModal.classList.add('is-active');
// Tenta preencher a pasta atual automaticamente baseado na navegação
const currentPath = currentPathStack.join('/');
subfolderInput.value = currentPath;
subfolderInput.value = currentPathStack.join('/');
switchTab('file'); // Reset para aba de arquivo
} else {
uploadModal.classList.remove('is-active');
uploadStatus.classList.add('is-hidden');
uploadProgress.classList.add('is-hidden');
stopRecordingLogic();
audioBlob = null; // Limpa gravação
recordPreview.src = "";
fileInput.value = ""; // Limpa input file
fileNameDisplay.textContent = "Nenhum selecionado";
}
}
@ -527,33 +644,49 @@ document.addEventListener('DOMContentLoaded', () => {
if(btnCloseUpload) btnCloseUpload.onclick = () => toggleUploadModal(false);
if(btnCancelUpload) btnCancelUpload.onclick = () => toggleUploadModal(false);
// Atualizar nome do arquivo no input
fileInput.onchange = () => {
if (fileInput.files.length > 0) {
fileNameDisplay.textContent = fileInput.files[0].name;
}
if (fileInput.files.length > 0) fileNameDisplay.textContent = fileInput.files[0].name;
};
// Lógica de envio robusta com debug
// ENVIO (Upload)
confirmUploadBtn.onclick = async (e) => {
e.preventDefault();
if (fileInput.files.length === 0) {
alert("Selecione um arquivo primeiro.");
return;
const formData = new FormData();
// Adiciona pasta
formData.append('subfolder', subfolderInput.value);
// Verifica qual aba está ativa para pegar o arquivo correto
if (activeTab === 'file') {
if (fileInput.files.length === 0) {
alert("Selecione um arquivo primeiro.");
return;
}
formData.append('sample_file', fileInput.files[0]);
} else {
// ABA MICROFONE
if (!audioBlob) {
alert("Grave algo antes de enviar.");
return;
}
let filename = recordNameInput.value.trim() || "gravacao";
// Força extensão .webm se não tiver, para garantir que o server entenda
if(!filename.match(/\.(webm|ogg|wav)$/i)) {
filename += ".webm";
}
formData.append('sample_file', audioBlob, filename);
}
const formData = new FormData(document.getElementById('sample-upload-form'));
const API_URL = 'https://alice.ufsj.edu.br:33002/api/upload/sample';
// UI de Carregamento
// UI Loading
confirmUploadBtn.classList.add('is-loading');
uploadProgress.classList.remove('is-hidden');
uploadStatus.classList.remove('is-hidden');
uploadStatus.textContent = "Iniciando conexão com o servidor...";
uploadStatus.textContent = "Enviando dados...";
uploadStatus.className = "help has-text-info";
console.log("Tentando upload para:", API_URL);
console.log("Enviando para:", API_URL);
try {
const response = await fetch(API_URL, {
@ -561,29 +694,22 @@ document.addEventListener('DOMContentLoaded', () => {
body: formData
});
console.log("Status da resposta:", response.status);
console.log("Status:", response.status);
const result = await response.json();
if (response.ok) {
uploadStatus.textContent = "Sucesso! Recarregando em 2s...";
uploadStatus.textContent = "Sucesso! Recarregando...";
uploadStatus.className = "help has-text-success";
// Aguarda um pouco e recarrega a página para pegar o novo manifesto
setTimeout(() => {
window.location.reload();
}, 2000);
setTimeout(() => window.location.reload(), 2000);
} else {
throw new Error(result.error || "O servidor recusou o arquivo (Erro " + response.status + ")");
throw new Error(result.error || "Erro no servidor (" + response.status + ")");
}
} catch (error) {
console.error("Erro detalhado do Upload:", error);
console.error("Erro Upload:", error);
let msg = "Erro: " + error.message;
if (error.message.includes("Failed to fetch")) {
msg = "Erro de Conexão: O servidor não respondeu. Verifique se você está na VPN/Rede da UFSJ ou se a porta 33002 está bloqueada.";
msg = "Erro de Conexão (Verifique VPN/Firewall - Porta 33002).";
}
uploadStatus.textContent = msg;
uploadStatus.className = "help has-text-danger";
confirmUploadBtn.classList.remove('is-loading');