|
362 | 362 | // ============================================================================ |
363 | 363 | const S = { files: [], tree: [], root: 'project', ctx: '', busy: false, rendered: 0, basePath: '', basePathSet: false, model: 'gpt' }; |
364 | 364 | const $ = id => document.getElementById(id); |
| 365 | + const $safe = id => { |
| 366 | + const el = document.getElementById(id); |
| 367 | + if (!el) console.warn(`Missing DOM element with id="${id}"`); |
| 368 | + return el; |
| 369 | + }; |
365 | 370 | const D = { |
366 | | - side: $('sidebar'), tog: $('toggleSidebar'), tree: $('fileTree'), |
367 | | - search: $('fileSearch'), sel: $('selectDirBtn'), |
368 | | - exp: $('expandAll'), col: $('collapseAll'), |
369 | | - all: $('selectAll'), none: $('deselectAll'), |
370 | | - gen: $('generateContextBtn'), ed: $('codeEditor'), |
371 | | - cnt: $('fileCount'), tok: $('tokenCount'), sz: $('totalSize'), |
372 | | - pron: $('tokenPronunciation'), lang: $('languagesList'), |
373 | | - copy: $('copyBtn'), txt: $('downloadTxtBtn'), |
374 | | - load: $('loadingOverlay'), toast: $('toastContainer'), |
375 | | - model: $('modelSelector'), |
376 | | - loadingText: document.getElementById('loadingText') |
| 371 | + side: $safe('sidebar'), tog: $safe('toggleSidebar'), tree: $safe('fileTree'), |
| 372 | + search: $safe('fileSearch'), sel: $safe('selectDirBtn'), |
| 373 | + exp: $safe('expandAll'), col: $safe('collapseAll'), |
| 374 | + all: $safe('selectAll'), none: $safe('deselectAll'), |
| 375 | + gen: $safe('generateContextBtn'), ed: $safe('codeEditor'), |
| 376 | + cnt: $safe('fileCount'), tok: $safe('tokenCount'), sz: $safe('totalSize'), |
| 377 | + pron: $safe('tokenPronunciation'), lang: $safe('languagesList'), |
| 378 | + copy: $safe('copyBtn'), txt: $safe('downloadTxtBtn'), |
| 379 | + load: $safe('loadingOverlay'), toast: $safe('toastContainer'), |
| 380 | + model: $safe('modelSelector'), |
| 381 | + loadingText: $safe('loadingText') |
377 | 382 | }; |
378 | 383 | const inp = document.createElement('input'); |
379 | | - inp.type = 'file'; inp.webkitdirectory = true; inp.multiple = true; inp.style.display = 'none'; |
| 384 | + inp.type = 'file'; |
| 385 | + inp.multiple = true; |
| 386 | + inp.setAttribute('webkitdirectory', ''); |
| 387 | + inp.setAttribute('mozdirectory', ''); |
| 388 | + inp.setAttribute('directory', ''); |
| 389 | + inp.style.display = 'none'; |
380 | 390 | document.body.appendChild(inp); |
381 | 391 |
|
382 | 392 | // ============================================================================ |
|
1249 | 1259 | for (let i = 0; i < textFiles.length; i += FAST_BATCH) { |
1250 | 1260 | const batch = textFiles.slice(i, i + FAST_BATCH); |
1251 | 1261 | const results = await Promise.allSettled( |
1252 | | - batch.map(f => f.file.text()) // Only read files from the 'textFiles' list |
| 1262 | + batch.map(f => f.file.text()) |
1253 | 1263 | ); |
1254 | 1264 | results.forEach((result, idx) => { |
1255 | 1265 | if (result.status === 'fulfilled') { |
1256 | 1266 | const content = result.value; |
1257 | 1267 | const p = batch[idx].path; |
1258 | | - const loadFiles = async list => { |
| 1268 | + const filename = batch[idx].name; |
| 1269 | + |
| 1270 | + if (content.indexOf('\0') !== -1) { |
| 1271 | + console.warn('Download: Skipping file with null bytes:', filename); |
| 1272 | + return; |
| 1273 | + } |
| 1274 | + |
| 1275 | + const lines = content.split('\n'); |
| 1276 | + const longBase64Lines = lines.filter(line => |
| 1277 | + line.length > 200 && /^[A-Za-z0-9+/=]+$/.test(line.trim()) |
| 1278 | + ).length; |
| 1279 | + |
| 1280 | + if (longBase64Lines > 10) { |
| 1281 | + console.warn('Download: Skipping file with encoded data:', filename); |
| 1282 | + return; |
| 1283 | + } |
| 1284 | + |
| 1285 | + const hasVeryLongLine = lines.some(line => line.length > 10000); |
| 1286 | + if (hasVeryLongLine) { |
| 1287 | + console.warn('Download: Skipping minified file:', filename); |
| 1288 | + return; |
| 1289 | + } |
| 1290 | + |
| 1291 | + parts.push('=== FILE: ' + getFullPath(p) + ' ===\n'); |
| 1292 | + parts.push('<document path="' + p + '">\n', content, '\n</document>\n'); |
| 1293 | + |
| 1294 | + const sFileIndex = S.files.findIndex(sf => sf.path === p); |
| 1295 | + if (sFileIndex !== -1) { |
| 1296 | + S.files[sFileIndex].file = null; |
| 1297 | + } |
| 1298 | + } else { |
| 1299 | + console.warn('Skip', batch[idx].name, result.reason); |
| 1300 | + } |
| 1301 | + }); |
| 1302 | + done += batch.length; |
| 1303 | + const pct = Math.round(done / textFiles.length * 100); |
| 1304 | + const status = []; |
| 1305 | + status.push(`${done}/${textFiles.length}`); |
| 1306 | + if (binaryFiles && binaryFiles.length > 0) { |
| 1307 | + status.push(`${binaryFiles.length} binary`); |
| 1308 | + } |
| 1309 | + D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">download</span><p>Preparing: ${pct}%</p><small>${status.join(' • ')}</small></div>`; |
| 1310 | + |
| 1311 | + if (i % (FAST_BATCH * 5) === 0) { |
| 1312 | + await new Promise(r => setTimeout(r, 0)); |
| 1313 | + } |
| 1314 | + } |
| 1315 | + |
| 1316 | + await new Promise(r => setTimeout(r, 0)); |
| 1317 | + |
| 1318 | + const blob = new Blob(parts, { type: 'text/plain' }); |
| 1319 | + const url = URL.createObjectURL(blob); |
| 1320 | + const a = document.createElement('a'); |
| 1321 | + a.href = url; |
| 1322 | + a.download = `${S.root}-context.txt`; |
| 1323 | + a.click(); |
| 1324 | + URL.revokeObjectURL(url); |
| 1325 | + |
| 1326 | + const stats = []; |
| 1327 | + stats.push(`${textFiles.length} files`); |
| 1328 | + if (binaryFiles && binaryFiles.length > 0) { |
| 1329 | + stats.push(`${binaryFiles.length} binary`); |
| 1330 | + } |
| 1331 | + stats.push(bytes(blob.size)); |
| 1332 | + D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">check_circle</span><p>Downloaded!</p><small>File: ${S.root}-context.txt</small><small>${stats.join(' • ')}</small></div>`; |
| 1333 | + |
| 1334 | + parts.length = 0; |
| 1335 | + }; |
| 1336 | + |
| 1337 | + const loadFiles = async list => { |
1259 | 1338 | try { |
1260 | 1339 | // Clear previous memory before loading new files |
1261 | 1340 | clearMemory(); |
|
1381 | 1460 | // ============================================================================ |
1382 | 1461 | const setup = () => { |
1383 | 1462 | console.log('Setting up event handlers...'); |
1384 | | - D.tog.onclick = () => D.side.classList.toggle('collapsed'); |
1385 | | - D.sel.onclick = () => { |
1386 | | - console.log('Select folder clicked'); |
1387 | | - inp.click(); |
1388 | | - }; |
| 1463 | + if (D.tog) D.tog.addEventListener('click', () => D.side && D.side.classList.toggle('collapsed')); |
| 1464 | + |
| 1465 | + if (D.sel) { |
| 1466 | + D.sel.addEventListener('click', () => { |
| 1467 | + console.log('Select folder clicked'); |
| 1468 | + try { |
| 1469 | + inp.click(); |
| 1470 | + } catch (e) { |
| 1471 | + console.error('Failed to open directory picker:', e); |
| 1472 | + toast('Directory picker blocked', 'error'); |
| 1473 | + } |
| 1474 | + }); |
| 1475 | + } else { |
| 1476 | + console.warn('selectDirBtn not found — directory selection unavailable'); |
| 1477 | + } |
1389 | 1478 | // Model selector event |
1390 | 1479 | if (D.model) { |
1391 | | - D.model.onchange = () => { |
| 1480 | + D.model.addEventListener('change', () => { |
1392 | 1481 | S.model = D.model.value; |
1393 | | - }; |
| 1482 | + }); |
1394 | 1483 | } |
1395 | 1484 | // Optimize file input change handler |
1396 | 1485 | inp.onchange = e => { |
|
1423 | 1512 | stats(); |
1424 | 1513 | } |
1425 | 1514 | }; |
1426 | | - D.search.oninput = e => { |
| 1515 | + if (D.search) D.search.addEventListener('input', e => { |
1427 | 1516 | const q = e.target.value.toLowerCase(); |
1428 | 1517 | document.querySelectorAll('.tree-item').forEach(item => { |
1429 | 1518 | const n = item.querySelector('.file-name').textContent.toLowerCase(); |
1430 | 1519 | item.style.display = n.includes(q) ? '' : 'none'; |
1431 | 1520 | }); |
1432 | | - }; |
| 1521 | + }); |
1433 | 1522 | const togFold = o => { |
1434 | 1523 | document.querySelectorAll('.expand-btn').forEach(btn => { |
1435 | 1524 | const item = btn.closest('.tree-item'); |
|
1445 | 1534 | document.querySelectorAll('.file-checkbox:not([disabled])').forEach(cb => cb.checked = c); |
1446 | 1535 | stats(); |
1447 | 1536 | }; |
1448 | | - D.exp.onclick = () => togFold(true); |
1449 | | - D.col.onclick = () => togFold(false); |
1450 | | - D.all.onclick = () => togCheck(true); |
1451 | | - D.none.onclick = () => togCheck(false); |
1452 | | - D.gen.onclick = gen; |
1453 | | - D.copy.onclick = copyClip; |
1454 | | - D.txt.onclick = () => dl('txt'); |
| 1537 | + if (D.exp) D.exp.addEventListener('click', () => togFold(true)); |
| 1538 | + if (D.col) D.col.addEventListener('click', () => togFold(false)); |
| 1539 | + if (D.all) D.all.addEventListener('click', () => togCheck(true)); |
| 1540 | + if (D.none) D.none.addEventListener('click', () => togCheck(false)); |
| 1541 | + if (D.gen) D.gen.addEventListener('click', gen); |
| 1542 | + if (D.copy) D.copy.addEventListener('click', copyClip); |
| 1543 | + if (D.txt) D.txt.addEventListener('click', () => dl('txt')); |
1455 | 1544 | // Clear All button |
1456 | 1545 | const clearBtn = $('clearAll'); |
1457 | 1546 | if (clearBtn) { |
1458 | 1547 | clearBtn.onclick = clearAll; |
1459 | 1548 | } |
1460 | | - document.onkeydown = e => { |
1461 | | - if ((e.ctrlKey || e.metaKey) && e.key === 'b') { e.preventDefault(); D.side.classList.toggle('collapsed'); } |
1462 | | - }; |
| 1549 | + document.addEventListener('keydown', e => { |
| 1550 | + if ((e.ctrlKey || e.metaKey) && e.key === 'b') { |
| 1551 | + e.preventDefault(); |
| 1552 | + if (D.side) D.side.classList.toggle('collapsed'); |
| 1553 | + } |
| 1554 | + }); |
1463 | 1555 | } |
1464 | 1556 | // ============================================================================ |
1465 | 1557 | // INIT |
|
1471 | 1563 | }; |
1472 | 1564 | if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); |
1473 | 1565 | else init(); |
1474 | | -})(); |
| 1566 | +}})(); |
0 commit comments