página de usuário
Deploy / Deploy (push) Successful in 1m23s
Details
Deploy / Deploy (push) Successful in 1m23s
Details
This commit is contained in:
parent
940d5e290e
commit
5709dee4ab
BIN
_data/users.db
BIN
_data/users.db
Binary file not shown.
186
pages/perfil.md
186
pages/perfil.md
|
|
@ -91,15 +91,20 @@ permalink: /perfil/
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table is-fullwidth is-hoverable is-striped">
|
<table class="table is-fullwidth is-hoverable is-striped">
|
||||||
<thead><tr><th>Nome do Projeto</th><th>Data</th><th class="has-text-right">Ações</th></tr></thead>
|
<thead><tr><th>Nome do Projeto</th><th>Data</th><th class="has-text-right">Ações</th></tr></thead>
|
||||||
<tbody id="projects-list">
|
<tbody id="projects-list"></tbody>
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
<p id="no-projects" class="has-text-grey is-hidden has-text-centered p-4">Nenhum projeto encontrado.</p>
|
<p id="no-projects" class="has-text-grey is-hidden has-text-centered p-4">Nenhum projeto encontrado.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="samples-tab" class="content-tab-content is-hidden">
|
<div id="samples-tab" class="content-tab-content is-hidden">
|
||||||
<p class="has-text-centered has-text-grey p-5">Em breve.</p>
|
<div class="table-container">
|
||||||
|
<table class="table is-fullwidth is-hoverable is-striped">
|
||||||
|
<thead><tr><th>Nome do Sample</th><th>Categoria</th><th>Data</th><th class="has-text-right">Ações</th></tr></thead>
|
||||||
|
<tbody id="samples-list"></tbody>
|
||||||
|
</table>
|
||||||
|
<p id="no-samples" class="has-text-grey is-hidden has-text-centered p-4">Nenhum sample enviado.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -108,18 +113,30 @@ permalink: /perfil/
|
||||||
|
|
||||||
<div id="preview-modal" class="modal">
|
<div id="preview-modal" class="modal">
|
||||||
<div class="modal-background"></div>
|
<div class="modal-background"></div>
|
||||||
<div class="modal-card" style="width: 90%; max-width: 900px; height: 80vh;">
|
<div class="modal-card" style="width: 90%; max-width: 900px;">
|
||||||
<header class="modal-card-head" style="background-color: #f0f8ff; border-bottom: 1px solid #cfe8fc;">
|
<header class="modal-card-head" style="background-color: #f0f8ff; border-bottom: 1px solid #cfe8fc;">
|
||||||
<p class="modal-card-title" id="modal-title" style="color: #205081; font-weight: bold;">Preview</p>
|
<p class="modal-card-title" id="modal-title" style="color: #205081; font-weight: bold;">Preview</p>
|
||||||
<button class="delete" aria-label="close"></button>
|
<button class="delete" aria-label="close"></button>
|
||||||
</header>
|
</header>
|
||||||
<section class="modal-card-body p-0" style="background-color: #fff; overflow: hidden;">
|
|
||||||
<iframe id="preview-iframe" src="" style="width: 100%; height: 100%; border: none;"></iframe>
|
<section class="modal-card-body p-0" style="background-color: #fff; min-height: 200px; display: flex; align-items: center; justify-content: center;">
|
||||||
|
|
||||||
|
<iframe id="preview-iframe" class="is-hidden" src="" style="width: 100%; height: 60vh; border: none;"></iframe>
|
||||||
|
|
||||||
|
<div id="audio-player-container" class="is-hidden p-5 has-text-centered" style="width: 100%;">
|
||||||
|
<div class="icon is-large has-text-info mb-4">
|
||||||
|
<i class="fa-solid fa-music fa-3x"></i>
|
||||||
|
</div>
|
||||||
|
<h4 id="audio-name" class="title is-5 mb-2"></h4>
|
||||||
|
<audio id="preview-audio" controls style="width: 80%;"></audio>
|
||||||
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="modal-card-foot" style="justify-content: flex-end; background-color: #fff; border-top: 1px solid #cfe8fc;">
|
<footer class="modal-card-foot" style="justify-content: flex-end; background-color: #fff; border-top: 1px solid #cfe8fc;">
|
||||||
<button class="button" id="close-modal-btn">Fechar</button>
|
<button class="button" id="close-modal-btn">Fechar</button>
|
||||||
<a href="#" id="full-edit-btn" target="_blank" class="button is-info">
|
<a href="#" id="full-edit-btn" target="_blank" class="button is-info">
|
||||||
<span>Abrir em Nova Aba</span>
|
<span>Abrir / Baixar</span>
|
||||||
<span class="icon is-small ml-1"><i class="fa-solid fa-up-right-from-square"></i></span>
|
<span class="icon is-small ml-1"><i class="fa-solid fa-up-right-from-square"></i></span>
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
@ -128,70 +145,85 @@ permalink: /perfil/
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// --- Lógica do Perfil ---
|
// Referências DOM
|
||||||
const els = {
|
const els = {
|
||||||
username: document.getElementById('display-username'),
|
username: document.getElementById('display-username'),
|
||||||
bio: document.getElementById('display-bio'),
|
bio: document.getElementById('display-bio'),
|
||||||
tags: document.getElementById('display-tags'),
|
tags: document.getElementById('display-tags'),
|
||||||
projectList: document.getElementById('projects-list'),
|
projectList: document.getElementById('projects-list'),
|
||||||
// ... (referências do form de edição iguais ao anterior)
|
sampleList: document.getElementById('samples-list'),
|
||||||
|
|
||||||
formBox: document.getElementById('edit-form-box'),
|
formBox: document.getElementById('edit-form-box'),
|
||||||
form: document.getElementById('profile-form'),
|
form: document.getElementById('profile-form'),
|
||||||
inpUser: document.getElementById('input-username'),
|
inpUser: document.getElementById('input-username'),
|
||||||
inpBio: document.getElementById('input-bio'),
|
inpBio: document.getElementById('input-bio'),
|
||||||
inpTags: document.getElementById('input-tags'),
|
inpTags: document.getElementById('input-tags'),
|
||||||
|
msg: document.getElementById('update-msg'),
|
||||||
|
|
||||||
btnEdit: document.getElementById('btn-edit-profile'),
|
btnEdit: document.getElementById('btn-edit-profile'),
|
||||||
btnCancel: document.getElementById('btn-cancel-edit'),
|
btnCancel: document.getElementById('btn-cancel-edit')
|
||||||
msg: document.getElementById('update-msg')
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Lógica do Modal ---
|
// Referências Modal
|
||||||
const modal = document.getElementById('preview-modal');
|
const modal = document.getElementById('preview-modal');
|
||||||
const iframe = document.getElementById('preview-iframe');
|
const iframe = document.getElementById('preview-iframe');
|
||||||
|
const audioContainer = document.getElementById('audio-player-container');
|
||||||
|
const audioPlayer = document.getElementById('preview-audio');
|
||||||
|
const audioName = document.getElementById('audio-name');
|
||||||
const modalTitle = document.getElementById('modal-title');
|
const modalTitle = document.getElementById('modal-title');
|
||||||
const fullEditBtn = document.getElementById('full-edit-btn');
|
const fullEditBtn = document.getElementById('full-edit-btn');
|
||||||
|
|
||||||
function openModal(url, title, fullLink) {
|
// --- FUNÇÃO DE MODAL INTELIGENTE ---
|
||||||
|
window.openModal = (type, url, title, fullLink) => {
|
||||||
modalTitle.textContent = title;
|
modalTitle.textContent = title;
|
||||||
iframe.src = url;
|
|
||||||
fullEditBtn.href = fullLink || url;
|
fullEditBtn.href = fullLink || url;
|
||||||
|
|
||||||
|
// Resetar
|
||||||
|
iframe.classList.add('is-hidden');
|
||||||
|
audioContainer.classList.add('is-hidden');
|
||||||
|
iframe.src = "";
|
||||||
|
audioPlayer.pause();
|
||||||
|
|
||||||
|
if (type === 'project') {
|
||||||
|
iframe.classList.remove('is-hidden');
|
||||||
|
iframe.src = url;
|
||||||
|
// Tenta esconder menu do iframe
|
||||||
|
iframe.onload = () => {
|
||||||
|
try {
|
||||||
|
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
const style = doc.createElement('style');
|
||||||
|
style.textContent = ".tabs, .navbar, footer { display: none !important; } body { overflow: auto !important; }";
|
||||||
|
doc.head.appendChild(style);
|
||||||
|
} catch(e){}
|
||||||
|
};
|
||||||
|
} else if (type === 'audio') {
|
||||||
|
audioContainer.classList.remove('is-hidden');
|
||||||
|
audioName.textContent = title.replace('Ouvir: ', '');
|
||||||
|
audioPlayer.src = url;
|
||||||
|
audioPlayer.play();
|
||||||
|
}
|
||||||
|
|
||||||
modal.classList.add('is-active');
|
modal.classList.add('is-active');
|
||||||
document.documentElement.classList.add('is-clipped');
|
document.documentElement.classList.add('is-clipped');
|
||||||
}
|
};
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
modal.classList.remove('is-active');
|
modal.classList.remove('is-active');
|
||||||
document.documentElement.classList.remove('is-clipped');
|
document.documentElement.classList.remove('is-clipped');
|
||||||
iframe.src = "";
|
iframe.src = "";
|
||||||
|
audioPlayer.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fecha modal nos botões X e Background
|
document.querySelectorAll('.modal-background, .modal-card-head .delete, #close-modal-btn').forEach(el => el.onclick = closeModal);
|
||||||
document.querySelectorAll('.modal-background, .modal-card-head .delete, #close-modal-btn').forEach(el => {
|
|
||||||
el.addEventListener('click', closeModal);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Limpa visual dentro do iframe para parecer um "modal nativo"
|
// --- CARREGAR DADOS ---
|
||||||
iframe.addEventListener('load', () => {
|
|
||||||
try {
|
|
||||||
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
|
||||||
const style = doc.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
.tabs, .navbar, .sidebar-wrapper, .main-header, footer { display: none !important; }
|
|
||||||
.publication { padding-top: 0 !important; }
|
|
||||||
body { background-color: #fff !important; overflow: auto !important; }
|
|
||||||
`;
|
|
||||||
doc.head.appendChild(style);
|
|
||||||
} catch(e) {}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Carregar Dados ---
|
|
||||||
async function loadProfile() {
|
async function loadProfile() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/user/profile');
|
const res = await fetch('/api/user/profile');
|
||||||
if (res.status === 401) { window.location.href = '/login/'; return; }
|
if (res.status === 401) { window.location.href = '/login/'; return; }
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
// Preenche Header
|
// Header
|
||||||
els.username.textContent = data.username;
|
els.username.textContent = data.username;
|
||||||
els.bio.textContent = data.bio || "Sem bio definida.";
|
els.bio.textContent = data.bio || "Sem bio definida.";
|
||||||
els.tags.innerHTML = '';
|
els.tags.innerHTML = '';
|
||||||
|
|
@ -199,93 +231,79 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
if(t.trim()) els.tags.innerHTML += `<span class="tag is-info is-light mr-1">${t.trim()}</span>`;
|
if(t.trim()) els.tags.innerHTML += `<span class="tag is-info is-light mr-1">${t.trim()}</span>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Preenche Forms
|
// Forms Values
|
||||||
els.inpUser.value = data.username;
|
els.inpUser.value = data.username;
|
||||||
els.inpBio.value = data.bio || "";
|
els.inpBio.value = data.bio || "";
|
||||||
els.inpTags.value = data.tags || "";
|
els.inpTags.value = data.tags || "";
|
||||||
|
|
||||||
// Preenche Lista de Projetos com Lógica de Link e Modal
|
// 1. Projetos
|
||||||
els.projectList.innerHTML = '';
|
els.projectList.innerHTML = '';
|
||||||
if (data.projects && data.projects.length > 0) {
|
if (data.projects && data.projects.length > 0) {
|
||||||
data.projects.forEach(p => {
|
data.projects.forEach(p => {
|
||||||
// Prepara URLs
|
|
||||||
// Assume que o arquivo mmp gera uma pagina em /projetos/NOME_SEM_EXTENSAO/
|
|
||||||
const slug = p.filename.replace('.mmp', '').replace('.mmpz', '');
|
const slug = p.filename.replace('.mmp', '').replace('.mmpz', '');
|
||||||
|
|
||||||
// URL da página pública do projeto
|
|
||||||
const pageUrl = `/mmpSearch/projetos/${slug}.html`;
|
const pageUrl = `/mmpSearch/projetos/${slug}.html`;
|
||||||
|
|
||||||
// URL do editor embedado
|
|
||||||
const editorUrl = `/mmpSearch/creation.html?project=${p.filename}&embed=true`;
|
const editorUrl = `/mmpSearch/creation.html?project=${p.filename}&embed=true`;
|
||||||
|
|
||||||
els.projectList.innerHTML += `
|
els.projectList.innerHTML += `
|
||||||
<tr>
|
<tr>
|
||||||
<td style="vertical-align: middle;">
|
<td style="vertical-align: middle;">
|
||||||
<a href="${pageUrl}" class="has-text-weight-bold" style="color: #205081; text-decoration: none;">
|
<a href="${pageUrl}" class="has-text-weight-bold" style="color: #205081;">
|
||||||
<span class="icon is-small mr-1"><i class="fa-solid fa-music"></i></span>
|
<span class="icon is-small mr-1"><i class="fa-solid fa-music"></i></span>${p.display_name}
|
||||||
${p.display_name}
|
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="vertical-align: middle;">${p.created_at}</td>
|
<td style="vertical-align: middle;">${p.created_at}</td>
|
||||||
<td class="has-text-right">
|
<td class="has-text-right">
|
||||||
<div class="buttons is-right">
|
<button class="button is-small is-info is-light" onclick="openModal('project', '${pageUrl}', 'Detalhes: ${p.display_name}', '${pageUrl}')"><i class="fa-solid fa-eye"></i></button>
|
||||||
<button class="button is-small is-info is-light"
|
<button class="button is-small is-warning is-light" onclick="openModal('project', '${editorUrl}', 'Editor: ${p.display_name}', '${editorUrl}')"><i class="fa-solid fa-sliders"></i></button>
|
||||||
onclick="openModal('${pageUrl}', 'Detalhes: ${p.display_name}', '${pageUrl}')"
|
<a href="${p.download_link}" class="button is-small is-primary is-light"><i class="fa-solid fa-download"></i></a>
|
||||||
title="Ver detalhes">
|
<button class="button is-small is-danger is-light" onclick="deleteItem('project', ${p.id})"><i class="fa-solid fa-trash"></i></button>
|
||||||
<i class="fa-solid fa-eye"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="button is-small is-warning is-light"
|
|
||||||
onclick="openModal('${editorUrl}', 'Editor: ${p.display_name}', '${editorUrl}')"
|
|
||||||
title="Editar no Navegador">
|
|
||||||
<i class="fa-solid fa-sliders"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href="${p.download_link}" class="button is-small is-primary is-light" title="Baixar MMP">
|
|
||||||
<i class="fa-solid fa-download"></i>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<button class="button is-small is-danger is-light" onclick="deleteProject(${p.id})" title="Excluir">
|
|
||||||
<i class="fa-solid fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>`;
|
||||||
`;
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('no-projects').classList.remove('is-hidden');
|
document.getElementById('no-projects').classList.remove('is-hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Samples
|
||||||
|
els.sampleList.innerHTML = '';
|
||||||
|
if (data.samples && data.samples.length > 0) {
|
||||||
|
data.samples.forEach(s => {
|
||||||
|
els.sampleList.innerHTML += `
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align: middle;"><strong>${s.display_name}</strong></td>
|
||||||
|
<td style="vertical-align: middle;"><span class="tag is-light">${s.category}</span></td>
|
||||||
|
<td style="vertical-align: middle;">${s.created_at}</td>
|
||||||
|
<td class="has-text-right">
|
||||||
|
<button class="button is-small is-success is-light" onclick="openModal('audio', '${s.download_link}', 'Ouvir: ${s.display_name}', '${s.download_link}')"><i class="fa-solid fa-play"></i></button>
|
||||||
|
<a href="${s.download_link}" download class="button is-small is-primary is-light"><i class="fa-solid fa-download"></i></a>
|
||||||
|
<button class="button is-small is-danger is-light" onclick="deleteItem('sample', ${s.id})"><i class="fa-solid fa-trash"></i></button>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document.getElementById('no-samples').classList.remove('is-hidden');
|
||||||
|
}
|
||||||
|
|
||||||
} catch(e) { console.error(e); }
|
} catch(e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Outros Handlers ---
|
// Handlers de Edição
|
||||||
els.btnEdit.onclick = () => els.formBox.classList.remove('is-hidden');
|
els.btnEdit.onclick = () => els.formBox.classList.remove('is-hidden');
|
||||||
els.btnCancel.onclick = () => els.formBox.classList.add('is-hidden');
|
els.btnCancel.onclick = () => els.formBox.classList.add('is-hidden');
|
||||||
|
|
||||||
// Handler de update (mesmo do anterior)
|
|
||||||
els.form.onsubmit = async (e) => {
|
els.form.onsubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// ... (Código de fetch /api/user/update igual ao anterior) ...
|
|
||||||
// Para economizar espaço, mantenha a lógica de POST aqui
|
|
||||||
// No sucesso, chame loadProfile() e esconda o form
|
|
||||||
|
|
||||||
// Simulação rápida para o exemplo:
|
|
||||||
const payload = { username: els.inpUser.value, bio: els.inpBio.value, tags: els.inpTags.value };
|
const payload = { username: els.inpUser.value, bio: els.inpBio.value, tags: els.inpTags.value };
|
||||||
const res = await fetch('/api/user/update', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(payload)});
|
const res = await fetch('/api/user/update', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(payload)});
|
||||||
if(res.ok) { els.formBox.classList.add('is-hidden'); loadProfile(); }
|
if(res.ok) { els.formBox.classList.add('is-hidden'); loadProfile(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Disponibiliza função global de delete
|
// Função Delete Genérica
|
||||||
window.deleteProject = async (id) => {
|
window.deleteItem = async (type, id) => {
|
||||||
if(!confirm("Apagar projeto permanentemente?")) return;
|
if(!confirm("Tem certeza que deseja apagar?")) return;
|
||||||
const res = await fetch(`/api/project/${id}`, { method: 'DELETE' });
|
const res = await fetch(`/api/${type}/${id}`, { method: 'DELETE' });
|
||||||
if(res.ok) loadProfile();
|
if(res.ok) loadProfile();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Disponibiliza função global para modal (usada no onclick inline)
|
|
||||||
window.openModal = openModal;
|
|
||||||
|
|
||||||
// Inicializa
|
// Inicializa
|
||||||
loadProfile();
|
loadProfile();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,10 @@ bcrypt = Bcrypt(app)
|
||||||
login_manager = LoginManager(app)
|
login_manager = LoginManager(app)
|
||||||
login_manager.login_view = 'login'
|
login_manager.login_view = 'login'
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# CLASSES UTILIZADAS
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
# --- MODELO DE USUÁRIO ---
|
# --- MODELO DE USUÁRIO ---
|
||||||
class User(UserMixin, db.Model):
|
class User(UserMixin, db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
@ -54,6 +58,8 @@ class User(UserMixin, db.Model):
|
||||||
cover_url = db.Column(db.String(255), default="/assets/img/default_cover.jpg")
|
cover_url = db.Column(db.String(255), default="/assets/img/default_cover.jpg")
|
||||||
|
|
||||||
projects = db.relationship('Project', backref='owner', lazy=True)
|
projects = db.relationship('Project', backref='owner', lazy=True)
|
||||||
|
samples = db.relationship('Sample', backref='owner', lazy=True)
|
||||||
|
recordings = db.relationship('Recording', backref='owner', lazy=True)
|
||||||
|
|
||||||
class SecureModelView(ModelView):
|
class SecureModelView(ModelView):
|
||||||
def is_accessible(self):
|
def is_accessible(self):
|
||||||
|
|
@ -76,6 +82,10 @@ class SecureIndexView(AdminIndexView):
|
||||||
|
|
||||||
return super(SecureIndexView, self).index()
|
return super(SecureIndexView, self).index()
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# CLASSES DE ARTEFATOS
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
# Tabela de Projetos
|
# Tabela de Projetos
|
||||||
class Project(db.Model):
|
class Project(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
@ -86,6 +96,21 @@ class Project(db.Model):
|
||||||
# Chave estrangeira ligando ao usuário
|
# Chave estrangeira ligando ao usuário
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
|
||||||
|
class Sample(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
filename = db.Column(db.String(255), nullable=False)
|
||||||
|
display_name = db.Column(db.String(255), nullable=False)
|
||||||
|
category = db.Column(db.String(50), default="Uncategorized") # Ex: drums, bass
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
|
||||||
|
class Recording(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
filename = db.Column(db.String(255), nullable=False)
|
||||||
|
display_name = db.Column(db.String(255), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
|
||||||
# Cria o banco na inicialização se não existir
|
# Cria o banco na inicialização se não existir
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
@ -322,30 +347,25 @@ def update_profile():
|
||||||
data = request.json
|
data = request.json
|
||||||
new_username = data.get('username')
|
new_username = data.get('username')
|
||||||
|
|
||||||
# Verificar disponibilidade do username se ele mudou
|
|
||||||
if new_username and new_username != current_user.username:
|
if new_username and new_username != current_user.username:
|
||||||
existing = User.query.filter_by(username=new_username).first()
|
if User.query.filter_by(username=new_username).first():
|
||||||
if existing:
|
return jsonify({"error": "Nome em uso."}), 400
|
||||||
return jsonify({"error": "Nome de usuário já está em uso."}), 400
|
|
||||||
current_user.username = new_username
|
current_user.username = new_username
|
||||||
|
|
||||||
if 'bio' in data:
|
if 'bio' in data: current_user.bio = data['bio'][:240]
|
||||||
current_user.bio = data['bio'][:240] # Corta em 240 chars
|
if 'tags' in data: current_user.tags = data['tags']
|
||||||
|
|
||||||
if 'tags' in data:
|
|
||||||
# Limpa e formata as tags
|
|
||||||
current_user.tags = data['tags']
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify({"message": "Perfil atualizado!"}), 200
|
return jsonify({"message": "Perfil atualizado!"}), 200
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return jsonify({"error": "Erro ao salvar."}), 500
|
return jsonify({"error": "Erro ao salvar."}), 500
|
||||||
|
|
||||||
# Atualize a rota user_profile antiga para retornar os novos dados
|
# Atualize a rota user_profile antiga para retornar os novos dados
|
||||||
@app.route('/api/user/profile', methods=['GET'])
|
@app.route('/api/user/profile', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
def user_profile():
|
def user_profile():
|
||||||
|
# Projetos
|
||||||
user_projects = []
|
user_projects = []
|
||||||
for p in current_user.projects:
|
for p in current_user.projects:
|
||||||
user_projects.append({
|
user_projects.append({
|
||||||
|
|
@ -356,6 +376,22 @@ def user_profile():
|
||||||
"download_link": f"/api/download/{p.filename}"
|
"download_link": f"/api/download/{p.filename}"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Samples
|
||||||
|
user_samples = []
|
||||||
|
for s in current_user.samples:
|
||||||
|
# Define a URL pública baseada na estrutura do seu Jekyll/Apache
|
||||||
|
# Ex: se SAMPLE_SRC é mapeado para /samples/ no navegador
|
||||||
|
# Ajuste o prefixo '/samples/' conforme sua configuração de pastas públicas
|
||||||
|
public_url = f"/samples/{s.category}/{s.filename}"
|
||||||
|
|
||||||
|
user_samples.append({
|
||||||
|
"id": s.id,
|
||||||
|
"display_name": s.display_name,
|
||||||
|
"category": s.category,
|
||||||
|
"created_at": s.created_at.strftime("%d/%m/%Y"),
|
||||||
|
"download_link": public_url
|
||||||
|
})
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"username": current_user.username,
|
"username": current_user.username,
|
||||||
"bio": current_user.bio,
|
"bio": current_user.bio,
|
||||||
|
|
@ -363,12 +399,9 @@ def user_profile():
|
||||||
"avatar": current_user.avatar_url,
|
"avatar": current_user.avatar_url,
|
||||||
"cover": current_user.cover_url,
|
"cover": current_user.cover_url,
|
||||||
"projects": user_projects,
|
"projects": user_projects,
|
||||||
"samples": [], # Placeholder enquanto não cria a tabela Samples
|
"samples": user_samples
|
||||||
"recordings": [] # Placeholder enquanto não cria a tabela Recordings
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/project/<int:project_id>', methods=['DELETE'])
|
@app.route('/api/project/<int:project_id>', methods=['DELETE'])
|
||||||
@login_required
|
@login_required
|
||||||
def delete_project(project_id):
|
def delete_project(project_id):
|
||||||
|
|
@ -393,6 +426,7 @@ def delete_project(project_id):
|
||||||
return jsonify({"message": "Projeto removido com sucesso"}), 200
|
return jsonify({"message": "Projeto removido com sucesso"}), 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# ROTAS PRINCIPAIS
|
# ROTAS PRINCIPAIS
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
@ -540,21 +574,69 @@ def resolve_samples():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/upload/sample', methods=['POST'])
|
@app.route('/api/upload/sample', methods=['POST'])
|
||||||
@login_required # <--- PROTEGIDO
|
@login_required
|
||||||
def upload_sample_standalone():
|
def upload_sample_standalone():
|
||||||
if 'sample_file' not in request.files: return jsonify({'error': 'Nenhum arquivo'}), 400
|
if 'sample_file' not in request.files:
|
||||||
|
return jsonify({'error': 'Nenhum arquivo'}), 400
|
||||||
|
|
||||||
file = request.files['sample_file']
|
file = request.files['sample_file']
|
||||||
subfolder = request.form.get('subfolder', '').strip()
|
# Pega a pasta escolhida pelo usuário, padrão 'uncategorized'
|
||||||
|
raw_subfolder = request.form.get('subfolder', 'uncategorized').strip()
|
||||||
|
|
||||||
|
# Limpa o nome da pasta para evitar ../../etc/passwd
|
||||||
|
safe_subfolder = secure_filename(raw_subfolder)
|
||||||
|
if not safe_subfolder:
|
||||||
|
safe_subfolder = 'uncategorized'
|
||||||
|
|
||||||
if file and allowed_sample(file.filename):
|
if file and allowed_sample(file.filename):
|
||||||
filename = secure_filename(file.filename)
|
filename = secure_filename(file.filename)
|
||||||
safe_subfolder = subfolder.replace('..', '').strip('/')
|
|
||||||
|
# Cria o caminho físico final: SAMPLE_SRC + Categoria + Arquivo
|
||||||
target_dir = os.path.join(SAMPLE_SRC, safe_subfolder)
|
target_dir = os.path.join(SAMPLE_SRC, safe_subfolder)
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
file.save(os.path.join(target_dir, filename))
|
|
||||||
generate_manifests(SRC_MMPSEARCH)
|
file_path = os.path.join(target_dir, filename)
|
||||||
run_jekyll_build()
|
file.save(file_path)
|
||||||
return jsonify({'message': 'Sample enviado!'}), 200
|
|
||||||
return jsonify({'error': 'Erro no arquivo'}), 400
|
# Salva no Banco
|
||||||
|
new_sample = Sample(
|
||||||
|
filename=filename,
|
||||||
|
display_name=filename,
|
||||||
|
category=safe_subfolder,
|
||||||
|
owner=current_user
|
||||||
|
)
|
||||||
|
db.session.add(new_sample)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Opcional: Atualizar índices se necessário
|
||||||
|
# generate_manifests(SRC_MMPSEARCH)
|
||||||
|
# run_jekyll_build()
|
||||||
|
|
||||||
|
return jsonify({'message': 'Sample enviado com sucesso!'}), 200
|
||||||
|
|
||||||
|
return jsonify({'error': 'Tipo de arquivo inválido'}), 400
|
||||||
|
|
||||||
|
@app.route('/api/sample/<int:sample_id>', methods=['DELETE'])
|
||||||
|
@login_required
|
||||||
|
def delete_sample(sample_id):
|
||||||
|
sample = Sample.query.get_or_404(sample_id)
|
||||||
|
|
||||||
|
if sample.owner != current_user and not current_user.is_admin:
|
||||||
|
return jsonify({"error": "Proibido"}), 403
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Tenta remover o arquivo físico
|
||||||
|
file_path = os.path.join(SAMPLE_SRC, sample.category, sample.filename)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
|
||||||
|
# 2. Remove do banco
|
||||||
|
db.session.delete(sample)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({"message": "Sample apagado"}), 200
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
# Inicializa o Flask-Admin
|
# Inicializa o Flask-Admin
|
||||||
admin = Admin(
|
admin = Admin(
|
||||||
|
|
@ -568,8 +650,6 @@ admin = Admin(
|
||||||
admin.add_view(SecureModelView(User, db.session, name="Gerenciar Usuários"))
|
admin.add_view(SecureModelView(User, db.session, name="Gerenciar Usuários"))
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# DEBUG: Isso vai mostrar no log todas as rotas que o Flask reconheceu
|
admin = Admin(app, name='MMP Admin', url='/api/admin', index_view=SecureIndexView())
|
||||||
print(app.url_map)
|
admin.add_view(SecureModelView(User, db.session, name="Gerenciar Usuários"))
|
||||||
|
|
||||||
print("Iniciando servidor na porta 33002 (HTTP Mode)...")
|
|
||||||
app.run(host="0.0.0.0", port=33002, debug=True)
|
app.run(host="0.0.0.0", port=33002, debug=True)
|
||||||
Loading…
Reference in New Issue