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:
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user