Projet

Général

Profil

Wiki » index.html

Emil Abutalibov, 02/03/2026 10:47

 
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FreeCAD → USB Upload</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }

body {
font-family: 'Segoe UI', Arial, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
color: #e0e0e0;
}

.card {
background: rgba(255,255,255,0.07);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 16px;
padding: 30px;
width: 100%;
max-width: 480px;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}

.logo {
text-align: center;
margin-bottom: 24px;
}

.logo svg { width: 56px; height: 56px; }

h1 {
font-size: 1.5rem;
text-align: center;
color: #fff;
margin-bottom: 6px;
}

.subtitle {
text-align: center;
font-size: 0.85rem;
color: #90caf9;
margin-bottom: 24px;
}

/* ── USB Status ── */
.usb-section {
margin-bottom: 20px;
}

.section-title {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 1px;
color: #90caf9;
margin-bottom: 10px;
}

.usb-list {
display: flex;
flex-direction: column;
gap: 8px;
}

.usb-item {
display: flex;
align-items: center;
gap: 10px;
background: rgba(255,255,255,0.06);
border-radius: 8px;
padding: 10px 14px;
border: 1px solid rgba(255,255,255,0.1);
}

.usb-icon { font-size: 1.3rem; }

.usb-info { flex: 1; }
.usb-name { font-size: 0.9rem; font-weight: 600; color: #fff; }
.usb-path { font-size: 0.75rem; color: #aaa; }

.usb-bar-wrap {
width: 60px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 3px;
}

.usb-bar-bg {
width: 60px; height: 6px;
background: rgba(255,255,255,0.15);
border-radius: 3px;
overflow: hidden;
}

.usb-bar-fill {
height: 100%;
background: linear-gradient(90deg, #4caf50, #81c784);
border-radius: 3px;
transition: width 0.3s;
}

.usb-free { font-size: 0.7rem; color: #aaa; }

.no-usb {
text-align: center;
padding: 14px;
background: rgba(255, 152, 0, 0.1);
border: 1px dashed rgba(255,152,0,0.4);
border-radius: 8px;
color: #ffb74d;
font-size: 0.85rem;
}

/* ── Upload Zone ── */
.drop-zone {
border: 2px dashed rgba(255,255,255,0.25);
border-radius: 12px;
padding: 30px 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 16px;
position: relative;
}

.drop-zone:hover, .drop-zone.drag-over {
border-color: #4fc3f7;
background: rgba(79, 195, 247, 0.08);
}

.drop-zone input[type="file"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
width: 100%;
height: 100%;
}

.drop-icon { font-size: 2.5rem; margin-bottom: 8px; }
.drop-text { font-size: 0.9rem; color: #ccc; }
.drop-hint { font-size: 0.75rem; color: #888; margin-top: 4px; }

.file-list {
margin-top: 10px;
text-align: left;
}

.file-chip {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(79,195,247,0.15);
border: 1px solid rgba(79,195,247,0.3);
border-radius: 20px;
padding: 3px 10px;
font-size: 0.78rem;
margin: 3px;
color: #e0e0e0;
}

button#uploadBtn {
width: 100%;
padding: 13px;
background: linear-gradient(135deg, #1976d2, #0d47a1);
color: white;
border: none;
border-radius: 10px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
letter-spacing: 0.5px;
}

button#uploadBtn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(25,118,210,0.4);
}

button#uploadBtn:disabled {
opacity: 0.5;
cursor: not-allowed;
}

/* ── Progress ── */
.progress-section { display: none; margin-top: 16px; }

.progress-bar-bg {
width: 100%; height: 8px;
background: rgba(255,255,255,0.1);
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}

.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #1976d2, #4fc3f7);
border-radius: 4px;
width: 0%;
transition: width 0.2s;
}

.progress-text {
font-size: 0.78rem;
color: #aaa;
text-align: center;
}

/* ── Results ── */
.results { margin-top: 16px; display: none; }

.result-file {
background: rgba(255,255,255,0.05);
border-radius: 10px;
padding: 12px 14px;
margin-bottom: 10px;
border-left: 3px solid #4caf50;
}

.result-file.has-error { border-left-color: #ef5350; }

.result-filename {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 6px;
color: #fff;
}

.result-usb {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.78rem;
padding: 4px 0;
border-bottom: 1px solid rgba(255,255,255,0.06);
}

.result-usb:last-child { border-bottom: none; }

.badge-ok { color: #81c784; }
.badge-err { color: #ef9a9a; }

@media (max-width: 480px) {
.card { padding: 20px; }
h1 { font-size: 1.2rem; }
}
</style>
</head>
<body>

<div class="card">

<div class="logo">
<!-- FreeCAD-like gear icon -->
<svg viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="28" cy="28" r="27" fill="rgba(25,118,210,0.2)" stroke="#1976d2" stroke-width="1.5"/>
<path d="M28 14 L32 20 L38 18 L38 24 L44 26 L40 30 L42 36 L36 36 L34 42 L28 40 L22 42 L20 36 L14 36 L16 30 L12 26 L18 24 L18 18 L24 20 Z" fill="#1976d2" opacity="0.7"/>
<circle cx="28" cy="28" r="7" fill="#4fc3f7"/>
</svg>
</div>

<h1>FreeCAD → USB</h1>
<p class="subtitle">Déposez vos fichiers FreeCAD pour les sauvegarder sur les clés USB</p>

<!-- USB Status -->
<div class="usb-section">
<div class="section-title">💾 Clés USB détectées</div>
<div class="usb-list" id="usbList">
<div class="no-usb">Chargement...</div>
</div>
<button onclick="refreshUSB()" style="margin-top:8px; background:none; border:1px solid rgba(255,255,255,0.2); color:#aaa; padding:5px 12px; border-radius:6px; cursor:pointer; font-size:0.75rem;">
↺ Actualiser
</button>
</div>

<!-- Upload zone -->
<div class="section-title">📁 Fichiers à envoyer</div>
<div class="drop-zone" id="dropZone">
<input type="file" id="fileInput" multiple accept=".FCStd,.fcstd,.step,.stp,.iges,.igs,.stl,.obj,.brep,.FCMacro">
<div class="drop-icon">📂</div>
<div class="drop-text">Cliquez ou glissez vos fichiers ici</div>
<div class="drop-hint">.FCStd, .STEP, .STL, .IGES, .BRep et autres</div>
<div class="file-list" id="fileList"></div>
</div>

<button id="uploadBtn" onclick="startUpload()" disabled>
Envoyer vers les clés USB
</button>

<!-- Progress -->
<div class="progress-section" id="progressSection">
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="progressFill"></div>
</div>
<div class="progress-text" id="progressText">Envoi en cours...</div>
</div>

<!-- Results -->
<div class="results" id="results"></div>

</div>

<script>
// ── USB detection ──────────────────────────────────────────────────────────

async function refreshUSB() {
const list = document.getElementById('usbList');
list.innerHTML = '<div class="no-usb">Détection en cours...</div>';
try {
const res = await fetch('/api/usb');
const drives = await res.json();
renderUSB(drives);
} catch(e) {
list.innerHTML = '<div class="no-usb">Erreur de connexion</div>';
}
}

function renderUSB(drives) {
const list = document.getElementById('usbList');
if (!drives.length) {
list.innerHTML = '<div class="no-usb">⚠️ Aucune clé USB branchée.<br>Branchez une clé USB et cliquez Actualiser.</div>';
return;
}
list.innerHTML = drives.map(d => {
const usedPct = ((d.total_gb - d.free_gb) / d.total_gb * 100).toFixed(0);
return `
<div class="usb-item">
<div class="usb-icon">🔌</div>
<div class="usb-info">
<div class="usb-name">${d.name}</div>
<div class="usb-path">${d.path}</div>
</div>
<div class="usb-bar-wrap">
<div class="usb-bar-bg">
<div class="usb-bar-fill" style="width:${usedPct}%"></div>
</div>
<div class="usb-free">${d.free_gb} GB libre</div>
</div>
</div>`;
}).join('');
}

// ── File selection ─────────────────────────────────────────────────────────

const fileInput = document.getElementById('fileInput');
const dropZone = document.getElementById('dropZone');

fileInput.addEventListener('change', updateFileList);

dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
dropZone.addEventListener('drop', e => {
e.preventDefault();
dropZone.classList.remove('drag-over');
fileInput.files = e.dataTransfer.files;
updateFileList();
});

function updateFileList() {
const files = fileInput.files;
const listEl = document.getElementById('fileList');
const btn = document.getElementById('uploadBtn');

if (!files.length) {
listEl.innerHTML = '';
btn.disabled = true;
return;
}

const sizeTotal = Array.from(files).reduce((s, f) => s + f.size, 0);
listEl.innerHTML = Array.from(files).map(f =>
`<span class="file-chip">📄 ${f.name} <span style="color:#888">${(f.size/1024/1024).toFixed(1)}MB</span></span>`
).join('');
btn.disabled = false;
}

// ── Upload ─────────────────────────────────────────────────────────────────

function startUpload() {
const files = fileInput.files;
if (!files.length) return;

const formData = new FormData();
for (let f of files) formData.append('files[]', f);

const btn = document.getElementById('uploadBtn');
const progressSection = document.getElementById('progressSection');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const results = document.getElementById('results');

btn.disabled = true;
btn.textContent = 'Envoi en cours...';
progressSection.style.display = 'block';
results.style.display = 'none';

const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload', true);

const startTime = Date.now();

xhr.upload.onprogress = e => {
if (!e.lengthComputable) return;
const pct = (e.loaded / e.total * 100).toFixed(1);
const elapsed = (Date.now() - startTime) / 1000;
const speed = (e.loaded / elapsed / 1024 / 1024).toFixed(2);
progressFill.style.width = pct + '%';
progressText.textContent = `${pct}% — ${(e.loaded/1024/1024).toFixed(1)} / ${(e.total/1024/1024).toFixed(1)} MB à ${speed} MB/s`;
};

xhr.onload = () => {
btn.textContent = 'Envoyer vers les clés USB';
btn.disabled = false;
progressSection.style.display = 'none';

try {
const data = JSON.parse(xhr.responseText);
showResults(data);
// Refresh USB list after upload
if (data.usb_drives) renderUSB(data.usb_drives);
} catch(e) {
showError("Réponse inattendue du serveur");
}
};

xhr.onerror = () => {
btn.textContent = 'Envoyer vers les clés USB';
btn.disabled = false;
progressSection.style.display = 'none';
showError("Erreur réseau");
};

xhr.send(formData);
}

function showResults(data) {
const results = document.getElementById('results');
results.style.display = 'block';

if (!data.uploaded || !data.uploaded.length) {
results.innerHTML = '<div class="no-usb">Aucun fichier reçu.</div>';
return;
}

results.innerHTML = '<div class="section-title" style="margin-bottom:10px">✅ Résultats</div>' +
data.uploaded.map(item => {
const hasErr = item.usb_results.some(r => !r.success);
return `
<div class="result-file ${hasErr ? 'has-error' : ''}">
<div class="result-filename">📄 ${item.file}</div>
${item.usb_results.map(r => `
<div class="result-usb">
<span class="${r.success ? 'badge-ok' : 'badge-err'}">${r.success ? '' : ''}</span>
<span style="flex:1">${r.drive}</span>
<span style="font-size:0.7rem;color:#888">${r.success ? r.path : (r.error || 'Erreur')}</span>
</div>
`).join('')}
</div>`;
}).join('') +
(data.errors && data.errors.length ? data.errors.map(e =>
`<div class="result-file has-error"><div class="result-filename">✗ ${e.file}</div><div style="font-size:0.78rem;color:#ef9a9a">${e.error}</div></div>`
).join('') : '');
}

function showError(msg) {
const results = document.getElementById('results');
results.style.display = 'block';
results.innerHTML = `<div class="no-usb" style="border-color:rgba(239,83,80,0.4);color:#ef9a9a">✗ ${msg}</div>`;
}

// ── Init ───────────────────────────────────────────────────────────────────
refreshUSB();
setInterval(refreshUSB, 15000); // Auto-refresh toutes les 15s
</script>

</body>
</html>
(2-2/3)