Pixel-Drive / app.js
NathMen12's picture
Create app.js
a1a0d92 verified
Raw
History Blame
28 kB
// ==========================================
// PIXELDRIVE v3.2 - APP.JS (Frontend)
// ==========================================
const app = {
user: null,
currentFolder: null,
currentSection: 'files',
files: [],
folders: [],
uploadController: null,
isMobile: window.innerWidth <= 768,
suppressClick: false,
// ---------- INIT ----------
async init() {
try {
const res = await fetch('/api/me');
if (res.ok) {
this.user = await res.json();
this.showApp();
if (!this.user.tosAccepted) this.showTos(true);
this.loadContent();
}
} catch (e) { console.error(e); }
document.addEventListener('click', (e) => {
const menu = document.getElementById('context-menu');
if (!menu.contains(e.target)) menu.style.display = 'none';
});
document.getElementById('file-area').addEventListener('contextmenu', (e) => e.preventDefault());
window.addEventListener('resize', () => {
this.isMobile = window.innerWidth <= 768;
if (!this.isMobile) {
document.getElementById('sidebar').classList.remove('open');
document.querySelector('.sidebar-overlay').classList.remove('active');
}
});
document.getElementById('login-pass').addEventListener('keydown', (e) => { if (e.key === 'Enter') this.login(); });
document.getElementById('reg-pass2').addEventListener('keydown', (e) => { if (e.key === 'Enter') this.register(); });
},
// ---------- UTILS ----------
esc(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
},
formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(0) + ' KB';
if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB';
return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB';
},
formatDate(ts) {
if (!ts) return '';
return new Date(ts).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
},
toast(msg, type) {
type = type || 'info';
const container = document.getElementById('toast-container');
const el = document.createElement('div');
el.className = 'toast ' + type;
el.textContent = msg;
container.appendChild(el);
setTimeout(() => {
el.style.opacity = '0';
el.style.transition = 'opacity 0.3s';
setTimeout(() => el.remove(), 300);
}, 3000);
},
// ---------- AUTH ----------
toggleAuth(mode) {
document.getElementById('login-form').style.display = mode === 'login' ? 'block' : 'none';
document.getElementById('register-form').style.display = mode === 'register' ? 'block' : 'none';
},
async login() {
const u = document.getElementById('login-user').value.trim();
const p = document.getElementById('login-pass').value;
if (!u || !p) return this.toast('Champs requis', 'error');
try {
const res = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ u, p }) });
const data = await res.json();
if (data.ok) {
this.user = data.user;
this.showApp();
if (!this.user.tosAccepted) this.showTos(true);
this.navTo('files');
} else {
this.toast(data.error || 'Erreur connexion', 'error');
}
} catch (e) { this.toast('Erreur réseau', 'error'); }
},
async register() {
const u = document.getElementById('reg-user').value.trim();
const p = document.getElementById('reg-pass').value;
const p2 = document.getElementById('reg-pass2').value;
const tos = document.getElementById('reg-tos').checked;
if (!u || !p) return this.toast('Champs requis', 'error');
if (p !== p2) return this.toast('Mots de passe différents', 'error');
if (!tos) return this.toast('Acceptez les CGU', 'error');
try {
const res = await fetch('/api/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ u, p, t: true }) });
const data = await res.json();
if (data.ok) {
this.toast('Compte créé !', 'success');
this.toggleAuth('login');
} else {
this.toast(data.error, 'error');
}
} catch (e) { this.toast('Erreur', 'error'); }
},
async logout() {
if (!confirm('Se déconnecter ?')) return;
await fetch('/api/logout', { method: 'POST' });
location.reload();
},
showApp() {
document.getElementById('auth-screen').style.display = 'none';
document.getElementById('app-screen').style.display = 'flex';
document.getElementById('user-display').textContent = this.user.username;
},
// ---------- TOS ----------
async showTos(force) {
const modal = document.getElementById('tos-modal');
const text = document.getElementById('tos-text');
const btn = document.getElementById('tos-accept-btn');
modal.style.display = 'flex';
try {
const res = await fetch('/api/tos');
text.textContent = await res.text();
} catch (e) { text.textContent = 'Erreur chargement CGU'; }
btn.style.display = force ? 'flex' : 'none';
},
hideTos() {
document.getElementById('tos-modal').style.display = 'none';
},
async acceptTos() {
await fetch('/api/tos/accept', { method: 'POST' });
this.user.tosAccepted = true;
this.hideTos();
this.toast('CGU acceptées', 'success');
},
// ---------- NAVIGATION ----------
toggleSidebar() {
document.getElementById('sidebar').classList.toggle('open');
document.querySelector('.sidebar-overlay').classList.toggle('active');
},
navTo(section) {
this.currentSection = section;
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
document.getElementById('nav-' + section).classList.add('active');
if (this.isMobile) this.toggleSidebar();
const actions = document.getElementById('toolbar-actions');
actions.style.display = (section === 'files') ? 'flex' : 'none';
if (section === 'files') {
this.currentFolder = null;
this.loadContent();
} else if (section === 'shared') {
this.loadSharedLinks();
} else if (section === 'recent') {
this.loadRecent();
}
},
goRoot() {
if (this.currentSection !== 'files') return this.navTo('files');
this.currentFolder = null;
this.loadContent();
},
refresh() {
if (this.currentSection === 'files') this.loadContent();
else if (this.currentSection === 'shared') this.loadSharedLinks();
else if (this.currentSection === 'recent') this.loadRecent();
},
// ---------- FILES ----------
async loadContent() {
const area = document.getElementById('file-area');
area.innerHTML = '<div style="padding: 2rem; text-align: center; color: var(--text-muted);">Chargement...</div>';
const bc = document.getElementById('breadcrumb');
bc.innerHTML = '<span onclick="app.goRoot()">Mes Fichiers</span>';
if (this.currentFolder) {
bc.innerHTML += ' <span class="sep">/</span> <span>' + this.esc(this.currentFolder.name) + '</span>';
}
try {
const [fRes, dRes] = await Promise.all([
fetch('/api/files?folder_id=' + (this.currentFolder ? this.currentFolder.id : '')),
fetch('/api/folders?parent_id=' + (this.currentFolder ? this.currentFolder.id : ''))
]);
this.files = await fRes.json();
this.folders = await dRes.json();
this.renderGrid(this.folders, this.files);
} catch (e) {
area.innerHTML = '<div style="padding: 2rem; color: var(--danger); text-align: center;">Erreur de chargement</div>';
}
},
async loadRecent() {
const area = document.getElementById('file-area');
area.innerHTML = '<div style="padding: 2rem; text-align: center; color: var(--text-muted);">Chargement...</div>';
document.getElementById('breadcrumb').innerHTML = '<span>Récents</span>';
try {
const all = [];
const walk = async (parentId) => {
const fRes = await fetch('/api/files?folder_id=' + (parentId || ''));
const files = await fRes.json();
files.forEach(f => all.push(f));
const dRes = await fetch('/api/folders?parent_id=' + (parentId || ''));
const folders = await dRes.json();
for (const d of folders) await walk(d.id);
};
await walk(null);
all.sort((a, b) => (b.updated_at || b.created_at || 0) - (a.updated_at || a.created_at || 0));
this.renderGrid([], all.slice(0, 20));
} catch (e) {
area.innerHTML = '<div style="padding: 2rem; color: var(--danger); text-align: center;">Erreur de chargement</div>';
}
},
renderGrid(folders, files) {
const area = document.getElementById('file-area');
area.innerHTML = '';
if (folders.length === 0 && files.length === 0) {
area.innerHTML = '<div class="empty-state"><svg class="icon icon-lg"><use href="#icon-folder"></use></svg><p>Dossier vide</p></div>';
return;
}
const grid = document.createElement('div');
grid.className = 'grid';
folders.forEach(f => {
const el = document.createElement('div');
el.className = 'folder-card';
el.innerHTML = '<svg class="icon"><use href="#icon-folder"></use></svg><span title="' + this.esc(f.name) + '">' + this.esc(f.name) + '</span>';
el.onclick = () => { this.currentFolder = f; this.loadContent(); };
el.oncontextmenu = (e) => this.showContextMenu(e, 'folder', f);
grid.appendChild(el);
});
files.forEach(f => {
const el = document.createElement('div');
el.className = 'file-card';
let thumbHtml = '<svg class="icon icon-placeholder"><use href="#icon-file"></use></svg>';
if (f.mime.startsWith('image/') || f.mime.startsWith('video/')) {
thumbHtml = '<img src="/api/files/' + f.id + '/thumb" alt="" loading="lazy">';
}
el.innerHTML =
'<div class="file-thumb">' + thumbHtml + '</div>' +
'<div class="file-info">' +
'<div class="file-name" title="' + this.esc(f.name) + '">' + this.esc(f.name) + '</div>' +
'<div class="file-meta"><span>' + this.formatSize(f.size) + '</span><span>' + (f.status === 'ready' ? 'Prêt' : this.esc(f.status)) + '</span></div>' +
'</div>';
el.onclick = () => {
if (this.suppressClick) { this.suppressClick = false; return; }
if (f.status === 'ready') this.previewFile(f);
};
el.oncontextmenu = (e) => this.showContextMenu(e, 'file', f);
let pressTimer = null;
el.addEventListener('touchstart', (e) => {
pressTimer = setTimeout(() => {
this.suppressClick = true;
this.showContextMenu(e, 'file', f);
}, 500);
}, { passive: true });
el.addEventListener('touchend', () => clearTimeout(pressTimer));
el.addEventListener('touchmove', () => clearTimeout(pressTimer));
grid.appendChild(el);
});
area.appendChild(grid);
},
// ---------- UPLOAD ----------
async handleUpload(input) {
if (!input.files.length) return;
const file = input.files[0];
const overlay = document.getElementById('upload-overlay');
const bar = document.getElementById('upload-progress');
const fname = document.getElementById('upload-filename');
const pct = document.getElementById('upload-percent');
overlay.style.display = 'block';
fname.textContent = file.name;
bar.style.width = '0%';
pct.textContent = '0%';
const formData = new FormData();
formData.append('file', file);
if (this.currentFolder) formData.append('folder_id', this.currentFolder.id);
this.uploadController = new AbortController();
try {
const res = await fetch('/api/files/upload', { method: 'POST', body: formData, signal: this.uploadController.signal });
if (!res.ok) throw new Error('Upload failed');
const data = await res.json();
this.listenProgress(data.fileId);
} catch (e) {
if (e.name !== 'AbortError') this.toast('Erreur upload', 'error');
overlay.style.display = 'none';
}
input.value = '';
},
listenProgress(fileId) {
const es = new EventSource('/api/files/' + fileId + '/progress');
const bar = document.getElementById('upload-progress');
const pct = document.getElementById('upload-percent');
const overlay = document.getElementById('upload-overlay');
es.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.type === 'progress') {
const p = Math.round(data.progress);
bar.style.width = p + '%';
pct.textContent = p + '%';
}
if (data.type === 'done' || data.type === 'error') {
es.close();
setTimeout(() => {
overlay.style.display = 'none';
if (this.currentSection === 'files') this.loadContent();
if (data.type === 'done') this.toast('Upload terminé', 'success');
else this.toast('Erreur upload', 'error');
}, 1000);
}
};
es.onerror = () => { es.close(); overlay.style.display = 'none'; };
},
cancelUpload() {
if (this.uploadController) this.uploadController.abort();
document.getElementById('upload-overlay').style.display = 'none';
},
async createFolder() {
const name = prompt('Nom du dossier :');
if (!name) return;
try {
const res = await fetch('/api/folders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ n: name, p: this.currentFolder ? this.currentFolder.id : null }) });
if (res.ok) this.loadContent();
else this.toast('Erreur création', 'error');
} catch (e) { this.toast('Erreur réseau', 'error'); }
},
// ---------- PREVIEW ----------
previewFile(file) {
const modal = document.getElementById('preview-modal');
const container = document.getElementById('preview-container');
const dlBtn = document.getElementById('preview-download-btn');
container.innerHTML = '';
modal.style.display = 'flex';
const url = '/api/files/' + file.id + '/download';
dlBtn.onclick = () => {
const a = document.createElement('a');
a.href = url;
a.download = file.name;
document.body.appendChild(a);
a.click();
a.remove();
};
if (file.mime.startsWith('image/')) {
const img = document.createElement('img');
img.src = url;
img.id = 'preview-content';
container.appendChild(img);
} else if (file.mime.startsWith('video/')) {
const vid = document.createElement('video');
vid.src = url;
vid.controls = true;
vid.autoplay = true;
vid.playsInline = true;
vid.id = 'preview-content';
container.appendChild(vid);
} else if (file.mime.startsWith('audio/')) {
const aud = document.createElement('audio');
aud.src = url;
aud.controls = true;
container.appendChild(aud);
} else {
container.innerHTML = '<div style="text-align:center; color:var(--text-main); padding: 2rem;"><svg class="icon" style="width:64px;height:64px;margin-bottom:1rem;"><use href="#icon-file"></use></svg><p style="margin-bottom:1rem; color: var(--text-muted);">Aperçu non disponible</p><p style="font-weight:500;">' + this.esc(file.name) + '</p></div>';
}
},
closePreview() {
const container = document.getElementById('preview-container');
const vid = container.querySelector('video');
const aud = container.querySelector('audio');
if (vid) vid.pause();
if (aud) aud.pause();
container.innerHTML = '';
document.getElementById('preview-modal').style.display = 'none';
},
// ---------- SHARE MODAL ----------
async showShareModal(file) {
const modal = document.getElementById('share-modal');
modal.style.display = 'flex';
const previewInput = document.getElementById('share-preview-url');
const downloadInput = document.getElementById('share-download-url');
const embedInput = document.getElementById('share-embed-code');
previewInput.value = 'Génération...';
downloadInput.value = 'Génération...';
embedInput.value = 'Génération...';
try {
const res = await fetch('/api/files/' + file.id + '/share', { method: 'POST' });
if (!res.ok) throw new Error('Erreur serveur');
const data = await res.json();
const origin = window.location.origin;
previewInput.value = origin + data.preview;
downloadInput.value = origin + data.download;
let embedCode = '';
if (file.mime.startsWith('image/')) {
embedCode = '<img src="' + origin + data.download + '" alt="' + this.esc(file.name) + '">';
} else if (file.mime.startsWith('video/')) {
embedCode = '<video controls src="' + origin + data.download + '" style="max-width:100%"></video>';
} else if (file.mime.startsWith('audio/')) {
embedCode = '<audio controls src="' + origin + data.download + '"></audio>';
} else {
embedCode = '<a href="' + origin + data.download + '" download="' + this.esc(file.name) + '">Télécharger ' + this.esc(file.name) + '</a>';
}
embedInput.value = embedCode;
document.querySelectorAll('.share-copy-btn').forEach(btn => {
btn.onclick = () => this.copyShare(btn.dataset.type);
});
} catch (e) {
this.toast('Erreur génération des liens', 'error');
modal.style.display = 'none';
}
},
closeShare() {
document.getElementById('share-modal').style.display = 'none';
},
async copyShare(type) {
let inputId = '';
if (type === 'preview') inputId = 'share-preview-url';
else if (type === 'download') inputId = 'share-download-url';
else inputId = 'share-embed-code';
const input = document.getElementById(inputId);
const text = input.value;
if (!text || text === 'Génération...') return;
try {
await navigator.clipboard.writeText(text);
} catch (e) {
input.select();
document.execCommand('copy');
}
const btn = document.querySelector('.share-copy-btn[data-type="' + type + '"]');
if (btn) {
btn.classList.add('copied');
setTimeout(() => btn.classList.remove('copied'), 1500);
}
this.toast('Lien copié !', 'success');
},
// ---------- SECTION PARTAGÉS ----------
async loadSharedLinks() {
const area = document.getElementById('file-area');
area.innerHTML = '<div style="padding: 2rem; text-align: center; color: var(--text-muted);">Chargement...</div>';
document.getElementById('breadcrumb').innerHTML = '<span>Liens Partagés</span>';
try {
const res = await fetch('/api/share-links');
const links = await res.json();
if (links.length === 0) {
area.innerHTML = '<div class="empty-state"><svg class="icon icon-lg"><use href="#icon-share"></use></svg><p>Aucun lien partagé</p><p style="font-size: 0.875rem; margin-top: 0.5rem;">Clic droit sur un fichier → Partager</p></div>';
return;
}
const grouped = {};
links.forEach(l => {
if (!grouped[l.file_id]) grouped[l.file_id] = { file_name: l.file_name, mime: l.mime, size: l.size, links: [] };
grouped[l.file_id].links.push(l);
});
const list = document.createElement('div');
list.className = 'share-list';
Object.entries(grouped).forEach(([fileId, data]) => {
const embedLink = data.links.find(l => l.type === 'embed');
const downloadLink = data.links.find(l => l.type === 'download');
const expires = this.formatDate(Math.max(...data.links.map(l => l.expires_at || 0)));
const origin = window.location.origin;
const item = document.createElement('div');
item.className = 'share-item';
item.innerHTML =
'<div class="share-item-header">' +
'<div class="share-item-info">' +
'<svg class="icon icon-sm"><use href="#icon-file"></use></svg>' +
'<span class="share-item-name">' + this.esc(data.file_name || 'Fichier supprimé') + '</span>' +
'</div>' +
'<button class="btn-icon share-delete-btn" data-file-id="' + fileId + '" title="Supprimer tous les liens">' +
'<svg class="icon icon-sm"><use href="#icon-trash"></use></svg>' +
'</button>' +
'</div>' +
'<div class="share-item-links">' +
(embedLink ?
'<div class="share-link-row"><span class="share-link-label">Aperçu</span><input type="text" readonly class="form-input share-link-input" value="' + origin + '/s/' + embedLink.token + '"><button class="btn-icon share-copy-btn" data-url="' + origin + '/s/' + embedLink.token + '"><svg class="icon icon-sm"><use href="#icon-link"></use></svg></button></div>'
: '') +
(downloadLink ?
'<div class="share-link-row"><span class="share-link-label">Download</span><input type="text" readonly class="form-input share-link-input" value="' + origin + '/d/' + downloadLink.token + '"><button class="btn-icon share-copy-btn" data-url="' + origin + '/d/' + downloadLink.token + '"><svg class="icon icon-sm"><use href="#icon-link"></use></svg></button></div>'
: '') +
'</div>' +
'<div class="share-item-footer">Expire le ' + expires + '</div>';
list.appendChild(item);
});
area.innerHTML = '';
area.appendChild(list);
area.querySelectorAll('.share-copy-btn').forEach(btn => {
btn.onclick = async () => {
try {
await navigator.clipboard.writeText(btn.dataset.url);
} catch (e) {
const input = btn.previousElementSibling;
input.select();
document.execCommand('copy');
}
btn.classList.add('copied');
setTimeout(() => btn.classList.remove('copied'), 1500);
this.toast('Lien copié !', 'success');
};
});
area.querySelectorAll('.share-delete-btn').forEach(btn => {
btn.onclick = async () => {
if (!confirm('Supprimer tous les liens de ce fichier ?')) return;
const fileId = btn.dataset.fileId;
const tokens = grouped[fileId].links.map(l => l.token);
for (const token of tokens) {
await fetch('/api/share-links/' + token, { method: 'DELETE' });
}
this.toast('Liens supprimés', 'success');
this.loadSharedLinks();
};
});
} catch (e) {
area.innerHTML = '<div style="padding: 2rem; color: var(--danger); text-align: center;">Erreur de chargement</div>';
}
},
// ---------- CONTEXT MENU ----------
showContextMenu(e, type, item) {
try { e.preventDefault(); } catch (err) {}
e.stopPropagation();
const menu = document.getElementById('context-menu');
menu.style.display = 'block';
if (this.isMobile) {
menu.style.left = '0';
menu.style.top = 'auto';
menu.style.bottom = '0';
menu.style.right = '0';
menu.style.width = '100%';
} else {
const cx = e.pageX || 0;
const cy = e.pageY || 0;
menu.style.left = cx + 'px';
menu.style.top = cy + 'px';
menu.style.bottom = 'auto';
menu.style.right = 'auto';
menu.style.width = 'auto';
const rect = menu.getBoundingClientRect();
if (rect.right > window.innerWidth) menu.style.left = (cx - rect.width) + 'px';
if (rect.bottom > window.innerHeight) menu.style.top = (cy - rect.height) + 'px';
}
const isFile = type === 'file';
document.getElementById('ctx-preview').style.display = isFile ? 'flex' : 'none';
document.getElementById('ctx-download').style.display = isFile ? 'flex' : 'none';
document.getElementById('ctx-share').style.display = isFile ? 'flex' : 'none';
document.getElementById('ctx-preview').onclick = () => { this.closeMenu(); this.previewFile(item); };
document.getElementById('ctx-download').onclick = () => {
this.closeMenu();
const a = document.createElement('a');
a.href = '/api/files/' + item.id + '/download';
a.download = item.name;
document.body.appendChild(a);
a.click();
a.remove();
};
document.getElementById('ctx-share').onclick = () => { this.closeMenu(); this.showShareModal(item); };
document.getElementById('ctx-delete').onclick = async () => {
this.closeMenu();
if (confirm('Supprimer ' + item.name + ' ?')) {
await fetch('/api/' + type + 's/' + item.id, { method: 'DELETE' });
this.refresh();
this.toast('Supprimé', 'success');
}
};
document.getElementById('ctx-rename').onclick = () => { this.closeMenu(); this.toast('Bientôt disponible', 'info'); };
document.getElementById('ctx-move').onclick = () => { this.closeMenu(); this.toast('Bientôt disponible', 'info'); };
},
closeMenu() {
document.getElementById('context-menu').style.display = 'none';
}
};
// ---------- START ----------
app.init();