Projektin ZIP-lataus projektikorttiin

Lataa .zip -nappi renderöidään projektikortin headeriin.
ZIP rakennetaan selaimessa ilman ulkoisia kirjastoja (CRC-32 + ZIP-rakenne inline).
Kansiorakenne säilyy: prompts/*.md -tiedostot menevät alihakemistoon.
This commit is contained in:
Jaakko Vanhala
2026-04-12 15:59:14 +03:00
parent cd67562a67
commit 8ee997cb56

View File

@@ -1341,6 +1341,7 @@ OUTPUT FORMAT:
`<span style="color:var(--purple);font-weight:600">${esc(name||'Projekti')} <span style="color:#8b949e;font-weight:normal">(${entries.length})</span></span>` + `<span style="color:var(--purple);font-weight:600">${esc(name||'Projekti')} <span style="color:#8b949e;font-weight:normal">(${entries.length})</span></span>` +
`<span style="display:flex;gap:6px">` + `<span style="display:flex;gap:6px">` +
`<button class="btn btn-muted" onclick="copyAllProjectFiles('${id}')">Kopioi kaikki</button>` + `<button class="btn btn-muted" onclick="copyAllProjectFiles('${id}')">Kopioi kaikki</button>` +
`<button class="btn btn-muted" onclick="downloadProjectZip('${id}','${esc(name||'projekti')}')">Lataa .zip</button>` +
`<button class="btn btn-green" onclick="openInEditor(window._projectFiles['${id}'])">Avaa editorissa</button>` + `<button class="btn btn-green" onclick="openInEditor(window._projectFiles['${id}'])">Avaa editorissa</button>` +
`</span></div>` + `</span></div>` +
`<div class="project-tabs">${tabs}</div>${panels}</div>`; `<div class="project-tabs">${tabs}</div>${panels}</div>`;
@@ -1360,6 +1361,84 @@ OUTPUT FORMAT:
const text = Object.entries(files).map(([n,c]) => '# --- ' + n + ' ---\n' + c).join('\n\n'); const text = Object.entries(files).map(([n,c]) => '# --- ' + n + ' ---\n' + c).join('\n\n');
navigator.clipboard.writeText(text); 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) { window.switchProjTab = function(id,i) {
document.querySelectorAll(`.project-tab[data-card="${id}"]`).forEach((t,j) => t.classList.toggle('active', j===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'); document.querySelectorAll(`.proj-panel[data-card="${id}"]`).forEach((p,j) => p.style.display = j===i ? '' : 'none');