704 lines
23 KiB
JavaScript
704 lines
23 KiB
JavaScript
// Global state
|
|
let currentPage = 'dashboard';
|
|
let cloneHistory = [];
|
|
let settings = {
|
|
outputDir: 'M:\\CLONEWEB\\cloned-sites',
|
|
timeout: 30,
|
|
retries: 3,
|
|
excludedDomains: 'facebook.com\ntwitter.com\ninstagram.com\ngoogle-analytics.com\ngoogletagmanager.com'
|
|
};
|
|
|
|
// Initialize app
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
initializeApp();
|
|
});
|
|
|
|
function initializeApp() {
|
|
setupNavigation();
|
|
setupThemeToggle();
|
|
setupForms();
|
|
checkServiceStatus();
|
|
loadDashboard();
|
|
loadHistory();
|
|
loadSettings();
|
|
|
|
// Auto-refresh status every 30 seconds
|
|
setInterval(checkServiceStatus, 30000);
|
|
}
|
|
|
|
// Navigation
|
|
function setupNavigation() {
|
|
const navItems = document.querySelectorAll('.nav-item');
|
|
|
|
navItems.forEach(item => {
|
|
item.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const page = item.getAttribute('data-page');
|
|
navigateToPage(page);
|
|
});
|
|
});
|
|
}
|
|
|
|
function navigateToPage(page) {
|
|
// Update nav items
|
|
document.querySelectorAll('.nav-item').forEach(item => {
|
|
item.classList.remove('active');
|
|
if (item.getAttribute('data-page') === page) {
|
|
item.classList.add('active');
|
|
}
|
|
});
|
|
|
|
// Update pages
|
|
document.querySelectorAll('.page').forEach(p => {
|
|
p.classList.remove('active');
|
|
});
|
|
document.getElementById(page).classList.add('active');
|
|
|
|
// Update page title
|
|
const titles = {
|
|
'dashboard': 'Dashboard',
|
|
'new-clone': 'Nova Clonagem',
|
|
'history': 'Histórico',
|
|
'settings': 'Configurações'
|
|
};
|
|
document.getElementById('pageTitle').textContent = titles[page] || page;
|
|
|
|
currentPage = page;
|
|
|
|
// Refresh data when navigating
|
|
if (page === 'dashboard') {
|
|
loadDashboard();
|
|
} else if (page === 'history') {
|
|
loadHistory();
|
|
}
|
|
}
|
|
|
|
// Theme Toggle
|
|
function setupThemeToggle() {
|
|
const themeToggle = document.getElementById('themeToggle');
|
|
|
|
// Load saved theme
|
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
|
if (savedTheme === 'dark') {
|
|
document.body.classList.add('dark-theme');
|
|
themeToggle.checked = true;
|
|
}
|
|
|
|
themeToggle.addEventListener('change', () => {
|
|
if (themeToggle.checked) {
|
|
document.body.classList.add('dark-theme');
|
|
localStorage.setItem('theme', 'dark');
|
|
} else {
|
|
document.body.classList.remove('dark-theme');
|
|
localStorage.setItem('theme', 'light');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Service Status - verifica o serviço WGET na porta 3002
|
|
async function checkServiceStatus() {
|
|
const statusIndicator = document.getElementById('serviceStatus');
|
|
|
|
try {
|
|
// Verificar o serviço WGET via proxy do servidor (mesma origem)
|
|
const response = await fetch('/api/health', {
|
|
method: 'GET'
|
|
});
|
|
|
|
if (response.ok) {
|
|
statusIndicator.className = 'status-indicator online';
|
|
statusIndicator.querySelector('.status-text').textContent = 'Serviço Online';
|
|
} else {
|
|
statusIndicator.className = 'status-indicator offline';
|
|
statusIndicator.querySelector('.status-text').textContent = 'Serviço Offline';
|
|
}
|
|
} catch (error) {
|
|
statusIndicator.className = 'status-indicator offline';
|
|
statusIndicator.querySelector('.status-text').textContent = 'Serviço Offline';
|
|
}
|
|
}
|
|
|
|
// Dashboard
|
|
function loadDashboard() {
|
|
// Load from localStorage
|
|
const history = JSON.parse(localStorage.getItem('cloneHistory') || '[]');
|
|
cloneHistory = history;
|
|
|
|
// Calculate stats
|
|
const totalSites = history.length;
|
|
const totalSize = history.reduce((sum, item) => sum + (item.size || 0), 0);
|
|
const lastClone = history.length > 0 ? history[0].timestamp : null;
|
|
const successCount = history.filter(item => item.success).length;
|
|
const successRate = totalSites > 0 ? Math.round((successCount / totalSites) * 100) : 100;
|
|
|
|
// Update stats
|
|
document.getElementById('totalSites').textContent = totalSites;
|
|
document.getElementById('totalSize').textContent = formatBytes(totalSize);
|
|
document.getElementById('lastClone').textContent = lastClone ? formatRelativeTime(lastClone) : 'Nunca';
|
|
document.getElementById('successRate').textContent = successRate + '%';
|
|
|
|
// Update recent clones list
|
|
const recentClonesList = document.getElementById('recentClonesList');
|
|
|
|
if (history.length === 0) {
|
|
recentClonesList.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-inbox"></i>
|
|
<h3>Nenhuma clonagem ainda</h3>
|
|
<p>Comece clonando seu primeiro site!</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const recentClones = history.slice(0, 5);
|
|
recentClonesList.innerHTML = recentClones.map(clone => `
|
|
<div class="clone-item">
|
|
<div class="clone-info">
|
|
<div class="clone-icon">
|
|
<i class="fas fa-${clone.success ? 'check' : 'times'}"></i>
|
|
</div>
|
|
<div class="clone-details">
|
|
<h4>${clone.url}</h4>
|
|
<p>${formatRelativeTime(clone.timestamp)} • ${formatBytes(clone.size || 0)} • ${clone.files || 0} arquivos</p>
|
|
</div>
|
|
</div>
|
|
<div class="clone-actions">
|
|
${clone.success ? `
|
|
<button class="btn btn-secondary btn-sm" onclick="openClonedSite('${clone.directory}')">
|
|
<i class="fas fa-external-link-alt"></i>
|
|
</button>
|
|
<button class="btn btn-secondary btn-sm" onclick="openCloneFolder('${clone.directory}')">
|
|
<i class="fas fa-folder-open"></i>
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function refreshDashboard() {
|
|
loadDashboard();
|
|
showToast('Dashboard atualizado!', 'success');
|
|
}
|
|
|
|
// Clone Form
|
|
function setupForms() {
|
|
// Clone form
|
|
const cloneForm = document.getElementById('cloneForm');
|
|
cloneForm.addEventListener('submit', handleCloneSubmit);
|
|
|
|
// Depth slider
|
|
const depthSlider = document.getElementById('depthLevel');
|
|
const depthValue = document.getElementById('depthValue');
|
|
|
|
depthSlider.addEventListener('input', () => {
|
|
depthValue.textContent = depthSlider.value + ' níveis';
|
|
});
|
|
}
|
|
|
|
async function handleCloneSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const cloneUrlEl = document.getElementById('cloneUrl');
|
|
const depthLevelEl = document.getElementById('depthLevel');
|
|
const includeImagesEl = document.getElementById('includeImages');
|
|
const includeCSSEl = document.getElementById('includeCSS');
|
|
const includeJSEl = document.getElementById('includeJS');
|
|
const convertLinksEl = document.getElementById('convertLinks');
|
|
|
|
if (!cloneUrlEl || !depthLevelEl || !includeImagesEl || !includeCSSEl || !includeJSEl || !convertLinksEl) {
|
|
console.error('Form elements not found');
|
|
showToast('Erro: Elementos do formulário não encontrados', 'error');
|
|
return;
|
|
}
|
|
|
|
const url = cloneUrlEl.value.trim();
|
|
const depth = parseInt(depthLevelEl.value);
|
|
const includeImages = includeImagesEl.checked;
|
|
const includeCSS = includeCSSEl.checked;
|
|
const includeJS = includeJSEl.checked;
|
|
const convertLinks = convertLinksEl.checked;
|
|
const cloneMethod = 'wget';
|
|
|
|
if (!url) {
|
|
showToast('Por favor, insira uma URL válida', 'error');
|
|
return;
|
|
}
|
|
|
|
// Validate URL
|
|
try {
|
|
new URL(url);
|
|
} catch {
|
|
showToast('URL inválida. Use o formato: https://exemplo.com', 'error');
|
|
return;
|
|
}
|
|
|
|
await startCloning(url, {
|
|
depth,
|
|
includeImages,
|
|
includeCSS,
|
|
includeJS,
|
|
convertLinks,
|
|
method: cloneMethod
|
|
});
|
|
}
|
|
|
|
async function startCloning(url, options) {
|
|
const progressCard = document.getElementById('progressCard');
|
|
const progressBar = document.getElementById('progressBar');
|
|
const progressMessage = document.getElementById('progressMessage');
|
|
const filesDownloaded = document.getElementById('filesDownloaded');
|
|
const sizeDownloaded = document.getElementById('sizeDownloaded');
|
|
const timeElapsed = document.getElementById('timeElapsed');
|
|
|
|
// Show progress card
|
|
progressCard.style.display = 'block';
|
|
progressBar.style.width = '0%';
|
|
|
|
const startTime = Date.now();
|
|
let progress = 0;
|
|
|
|
// Simulate progress
|
|
const progressInterval = setInterval(() => {
|
|
progress = Math.min(progress + Math.random() * 10, 90);
|
|
progressBar.style.width = progress + '%';
|
|
|
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
timeElapsed.textContent = elapsed + 's';
|
|
}, 1000);
|
|
|
|
const messages = options.method === 'smart' ? [
|
|
'🚀 Iniciando Smart Clone (Puppeteer)...',
|
|
'⏳ Renderizando página completa...',
|
|
'🖱️ Simulando interação do usuário...',
|
|
'📜 Carregando conteúdo sob demanda...',
|
|
'📦 Capturando recursos da rede...',
|
|
'🔄 Reescrevendo links dinâmicos...',
|
|
'✨ Otimizando clone final...'
|
|
] : [
|
|
'🚀 Iniciando clonagem com WGET...',
|
|
'⏳ Sem limite de tempo - pode demorar vários minutos...',
|
|
'🔍 Analisando estrutura do site...',
|
|
'📄 Baixando páginas HTML...',
|
|
'🎨 Baixando CSS e estilos...',
|
|
'⚡ Baixando JavaScript...',
|
|
'📦 Baixando imagens e assets...',
|
|
'🔗 Convertendo links para locais...',
|
|
'💪 Clonagem profissional em andamento...',
|
|
'✨ Finalizando clonagem...'
|
|
];
|
|
|
|
let messageIndex = 0;
|
|
progressMessage.textContent = messages[0];
|
|
|
|
const messageInterval = setInterval(() => {
|
|
messageIndex = (messageIndex + 1) % messages.length;
|
|
progressMessage.textContent = messages[messageIndex];
|
|
}, 4000);
|
|
|
|
try {
|
|
let response;
|
|
|
|
if (options.method === 'smart') {
|
|
response = await fetch('/api/smart-clone', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ url })
|
|
});
|
|
} else {
|
|
// WGET logic
|
|
response = await fetch('/api/wget-clone', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
url,
|
|
depth: options.depth
|
|
})
|
|
});
|
|
}
|
|
|
|
clearInterval(progressInterval);
|
|
clearInterval(messageInterval);
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
// Complete progress
|
|
progressBar.style.width = '100%';
|
|
progressMessage.textContent = '✅ Clonagem concluída com sucesso!';
|
|
|
|
// Update stats
|
|
const files = data.data?.files || data.data?.cloneInfo?.fileCount || data.data?.resources || data.files || 0;
|
|
const size = data.data?.size || data.data?.cloneInfo?.totalSize || data.size || 0;
|
|
|
|
filesDownloaded.textContent = files;
|
|
sizeDownloaded.textContent = formatBytes(size);
|
|
|
|
// Save to history
|
|
const cloneRecord = {
|
|
url,
|
|
timestamp: new Date().toISOString(),
|
|
success: true,
|
|
files: files,
|
|
size: size,
|
|
directory: data.data.path || data.data.directory,
|
|
depth: options.depth,
|
|
method: options.method
|
|
};
|
|
|
|
saveToHistory(cloneRecord);
|
|
|
|
// Show success message
|
|
setTimeout(() => {
|
|
showToast('Site clonado com sucesso!', 'success');
|
|
progressCard.style.display = 'none';
|
|
|
|
// Show success dialog
|
|
showSuccessDialog(cloneRecord);
|
|
}, 2000);
|
|
|
|
} else {
|
|
throw new Error(data.error || 'Erro desconhecido');
|
|
}
|
|
|
|
} catch (error) {
|
|
clearInterval(progressInterval);
|
|
clearInterval(messageInterval);
|
|
|
|
console.error('Clone error:', error);
|
|
progressCard.style.display = 'none';
|
|
|
|
// Save failed attempt to history
|
|
saveToHistory({
|
|
url,
|
|
timestamp: new Date().toISOString(),
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
|
|
showToast('Erro ao clonar site: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
function showSuccessDialog(cloneRecord) {
|
|
const dialog = document.createElement('div');
|
|
dialog.className = 'success-dialog';
|
|
dialog.innerHTML = `
|
|
<div class="success-dialog-content">
|
|
<div class="success-icon">
|
|
<i class="fas fa-check-circle"></i>
|
|
</div>
|
|
<h3>Site Clonado com Sucesso!</h3>
|
|
<p>${cloneRecord.url}</p>
|
|
<div class="success-stats">
|
|
<span><i class="fas fa-file"></i> ${cloneRecord.files} arquivos</span>
|
|
<span><i class="fas fa-hdd"></i> ${formatBytes(cloneRecord.size)}</span>
|
|
</div>
|
|
<div class="success-actions">
|
|
<button class="btn btn-primary" onclick="openClonedSite('${cloneRecord.directory}'); closeSuccessDialog()">
|
|
<i class="fas fa-external-link-alt"></i> Abrir Site
|
|
</button>
|
|
<button class="btn btn-secondary" onclick="openCloneFolder('${cloneRecord.directory}'); closeSuccessDialog()">
|
|
<i class="fas fa-folder-open"></i> Abrir Pasta
|
|
</button>
|
|
<button class="btn btn-secondary" onclick="closeSuccessDialog()">
|
|
Fechar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(dialog);
|
|
|
|
// Add styles
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.success-dialog {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0,0,0,0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 10000;
|
|
animation: fadeIn 0.3s;
|
|
}
|
|
.success-dialog-content {
|
|
background: white;
|
|
border-radius: 16px;
|
|
padding: 2rem;
|
|
max-width: 500px;
|
|
text-align: center;
|
|
animation: slideUp 0.3s;
|
|
}
|
|
.success-icon {
|
|
font-size: 4rem;
|
|
color: var(--success-color);
|
|
margin-bottom: 1rem;
|
|
}
|
|
.success-dialog-content h3 {
|
|
font-size: 1.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.success-dialog-content p {
|
|
color: var(--text-secondary);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.success-stats {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 2rem;
|
|
margin-bottom: 2rem;
|
|
padding: 1rem;
|
|
background: var(--bg-primary);
|
|
border-radius: 8px;
|
|
}
|
|
.success-stats span {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
.success-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
@keyframes slideUp {
|
|
from { transform: translateY(20px); opacity: 0; }
|
|
to { transform: translateY(0); opacity: 1; }
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
function closeSuccessDialog() {
|
|
const dialog = document.querySelector('.success-dialog');
|
|
if (dialog) {
|
|
dialog.remove();
|
|
}
|
|
}
|
|
|
|
// History
|
|
function loadHistory() {
|
|
const history = JSON.parse(localStorage.getItem('cloneHistory') || '[]');
|
|
cloneHistory = history;
|
|
|
|
const historyList = document.getElementById('historyList');
|
|
|
|
if (history.length === 0) {
|
|
historyList.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-history"></i>
|
|
<h3>Nenhum histórico</h3>
|
|
<p>Seus sites clonados aparecerão aqui</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
historyList.innerHTML = history.map((item, index) => `
|
|
<div class="history-item">
|
|
<div class="history-info">
|
|
<h4>${item.url}</h4>
|
|
<div class="history-meta">
|
|
<span><i class="fas fa-calendar"></i> ${formatDate(item.timestamp)}</span>
|
|
<span><i class="fas fa-${item.success ? 'check-circle' : 'times-circle'}"></i> ${item.success ? 'Sucesso' : 'Falhou'}</span>
|
|
${item.files ? `<span><i class="fas fa-file"></i> ${item.files} arquivos</span>` : ''}
|
|
${item.size ? `<span><i class="fas fa-hdd"></i> ${formatBytes(item.size)}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="clone-actions">
|
|
${item.success ? `
|
|
<button class="btn btn-secondary btn-sm" onclick="openClonedSite('${item.directory}')" title="Abrir Site">
|
|
<i class="fas fa-external-link-alt"></i>
|
|
</button>
|
|
<button class="btn btn-secondary btn-sm" onclick="openCloneFolder('${item.directory}')" title="Abrir Pasta">
|
|
<i class="fas fa-folder-open"></i>
|
|
</button>
|
|
<button class="btn btn-primary btn-sm" onclick="recloneSite('${item.url}')" title="Clonar Novamente">
|
|
<i class="fas fa-redo"></i>
|
|
</button>
|
|
` : ''}
|
|
<button class="btn btn-danger btn-sm" onclick="deleteHistoryItem(${index})" title="Remover">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function refreshHistory() {
|
|
loadHistory();
|
|
showToast('Histórico atualizado!', 'success');
|
|
}
|
|
|
|
function clearHistory() {
|
|
if (confirm('Tem certeza que deseja limpar todo o histórico? Esta ação não pode ser desfeita.')) {
|
|
localStorage.removeItem('cloneHistory');
|
|
cloneHistory = [];
|
|
loadHistory();
|
|
loadDashboard();
|
|
showToast('Histórico limpo com sucesso!', 'success');
|
|
}
|
|
}
|
|
|
|
function deleteHistoryItem(index) {
|
|
if (confirm('Remover este item do histórico?')) {
|
|
cloneHistory.splice(index, 1);
|
|
localStorage.setItem('cloneHistory', JSON.stringify(cloneHistory));
|
|
loadHistory();
|
|
loadDashboard();
|
|
showToast('Item removido!', 'success');
|
|
}
|
|
}
|
|
|
|
function recloneSite(url) {
|
|
document.getElementById('cloneUrl').value = url;
|
|
navigateToPage('new-clone');
|
|
showToast('URL carregada! Configure as opções e clique em "Iniciar Clonagem"', 'info');
|
|
}
|
|
|
|
function saveToHistory(record) {
|
|
cloneHistory.unshift(record);
|
|
// Keep only last 100 records
|
|
if (cloneHistory.length > 100) {
|
|
cloneHistory = cloneHistory.slice(0, 100);
|
|
}
|
|
localStorage.setItem('cloneHistory', JSON.stringify(cloneHistory));
|
|
}
|
|
|
|
// Settings
|
|
function loadSettings() {
|
|
const savedSettings = localStorage.getItem('cloneSettings');
|
|
if (savedSettings) {
|
|
settings = JSON.parse(savedSettings);
|
|
}
|
|
|
|
// Populate settings form (if needed)
|
|
}
|
|
|
|
function saveSettings() {
|
|
localStorage.setItem('cloneSettings', JSON.stringify(settings));
|
|
showToast('Configurações salvas!', 'success');
|
|
}
|
|
|
|
// Utility Functions
|
|
function openClonedSite(directory) {
|
|
const siteName = directory.replace(/\\/g, '/').split('/').pop();
|
|
window.open(`/api/preview/${siteName}`, '_blank');
|
|
}
|
|
|
|
function openCloneFolder(directory) {
|
|
showToast('Abrindo gerenciador de arquivos web...', 'info');
|
|
fetch('/api/open-folder', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ path: directory })
|
|
}).then(res => res.json())
|
|
.then(data => {
|
|
if (!data.success) {
|
|
showToast('Erro ao abrir pasta: ' + (data.error || ''), 'error');
|
|
} else if (data.url) {
|
|
window.open(data.url, '_blank');
|
|
}
|
|
})
|
|
.catch(err => console.error('Erro ao chamar open-folder:', err));
|
|
}
|
|
|
|
function showToast(message, type = 'info') {
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast toast-${type}`;
|
|
toast.innerHTML = `
|
|
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
|
|
<span>${message}</span>
|
|
`;
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
// Add styles if not already added
|
|
if (!document.getElementById('toast-styles')) {
|
|
const style = document.createElement('style');
|
|
style.id = 'toast-styles';
|
|
style.textContent = `
|
|
.toast {
|
|
position: fixed;
|
|
bottom: 2rem;
|
|
right: 2rem;
|
|
background: white;
|
|
padding: 1rem 1.5rem;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
z-index: 10000;
|
|
animation: slideInRight 0.3s, slideOutRight 0.3s 2.7s;
|
|
}
|
|
.toast-success { border-left: 4px solid var(--success-color); }
|
|
.toast-error { border-left: 4px solid var(--danger-color); }
|
|
.toast-info { border-left: 4px solid var(--info-color); }
|
|
.toast i { font-size: 1.2rem; }
|
|
.toast-success i { color: var(--success-color); }
|
|
.toast-error i { color: var(--danger-color); }
|
|
.toast-info i { color: var(--info-color); }
|
|
@keyframes slideInRight {
|
|
from { transform: translateX(400px); opacity: 0; }
|
|
to { transform: translateX(0); opacity: 1; }
|
|
}
|
|
@keyframes slideOutRight {
|
|
from { transform: translateX(0); opacity: 1; }
|
|
to { transform: translateX(400px); opacity: 0; }
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
setTimeout(() => {
|
|
toast.remove();
|
|
}, 3000);
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString('pt-BR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
function formatRelativeTime(dateString) {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diff = now - date;
|
|
|
|
const seconds = Math.floor(diff / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const hours = Math.floor(minutes / 60);
|
|
const days = Math.floor(hours / 24);
|
|
|
|
if (days > 0) return days === 1 ? 'Há 1 dia' : `Há ${days} dias`;
|
|
if (hours > 0) return hours === 1 ? 'Há 1 hora' : `Há ${hours} horas`;
|
|
if (minutes > 0) return minutes === 1 ? 'Há 1 minuto' : `Há ${minutes} minutos`;
|
|
return 'Agora mesmo';
|
|
}
|