diff --git a/network-poc/frontend/src/pages/index.astro b/network-poc/frontend/src/pages/index.astro index d08308e..e8bf42d 100644 --- a/network-poc/frontend/src/pages/index.astro +++ b/network-poc/frontend/src/pages/index.astro @@ -1341,6 +1341,7 @@ OUTPUT FORMAT: `${esc(name||'Projekti')} (${entries.length})` + `` + `` + + `` + `` + `` + `
${tabs}
${panels}`; @@ -1360,6 +1361,84 @@ OUTPUT FORMAT: const text = Object.entries(files).map(([n,c]) => '# --- ' + n + ' ---\n' + c).join('\n\n'); navigator.clipboard.writeText(text); }; + window.downloadProjectZip = function(id, name) { + const files = window._projectFiles[id]; + if (!files) return; + const enc = new TextEncoder(); + const entries = Object.entries(files); + const localParts = []; + const centralParts = []; + let offset = 0; + + for (const [fname, content] of entries) { + const nameBytes = enc.encode(fname); + const dataBytes = enc.encode(content); + const crc = crc32(dataBytes); + + // Local file header + const local = new Uint8Array(30 + nameBytes.length + dataBytes.length); + const lv = new DataView(local.buffer); + lv.setUint32(0, 0x04034b50, true); // signature + lv.setUint16(4, 20, true); // version needed + lv.setUint16(8, 8, true); // UTF-8 flag + lv.setUint32(14, crc, true); // CRC-32 + lv.setUint32(18, dataBytes.length, true); // compressed size + lv.setUint32(22, dataBytes.length, true); // uncompressed size + lv.setUint16(26, nameBytes.length, true); // filename length + local.set(nameBytes, 30); + local.set(dataBytes, 30 + nameBytes.length); + localParts.push(local); + + // Central directory entry + const central = new Uint8Array(46 + nameBytes.length); + const cv = new DataView(central.buffer); + cv.setUint32(0, 0x02014b50, true); // signature + cv.setUint16(4, 20, true); // version made by + cv.setUint16(6, 20, true); // version needed + cv.setUint16(8, 8, true); // UTF-8 flag + cv.setUint32(16, crc, true); + cv.setUint32(20, dataBytes.length, true); + cv.setUint32(24, dataBytes.length, true); + cv.setUint16(28, nameBytes.length, true); + cv.setUint32(42, offset, true); // local header offset + central.set(nameBytes, 46); + centralParts.push(central); + + offset += local.length; + } + + const centralSize = centralParts.reduce((s, c) => s + c.length, 0); + // End of central directory + const eocd = new Uint8Array(22); + const ev = new DataView(eocd.buffer); + ev.setUint32(0, 0x06054b50, true); + ev.setUint16(8, entries.length, true); + ev.setUint16(10, entries.length, true); + ev.setUint32(12, centralSize, true); + ev.setUint32(16, offset, true); + + const blob = new Blob([...localParts, ...centralParts, eocd], { type: 'application/zip' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = (name || 'projekti').replace(/[^a-z0-9_-]/gi, '_') + '.zip'; + a.click(); + URL.revokeObjectURL(url); + }; + + // CRC-32 (ZIP-standardin mukainen) + const crc32Table = new Uint32Array(256); + for (let i = 0; i < 256; i++) { + let c = i; + for (let j = 0; j < 8; j++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1); + crc32Table[i] = c; + } + function crc32(data) { + let crc = 0xFFFFFFFF; + for (let i = 0; i < data.length; i++) crc = crc32Table[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8); + return (crc ^ 0xFFFFFFFF) >>> 0; + } + window.switchProjTab = function(id,i) { document.querySelectorAll(`.project-tab[data-card="${id}"]`).forEach((t,j) => t.classList.toggle('active', j===i)); document.querySelectorAll(`.proj-panel[data-card="${id}"]`).forEach((p,j) => p.style.display = j===i ? '' : 'none');