// ── File Explorer Modal ── // Wiederverwendbares Modal zum Durchsuchen des Workspaces. // Nutzung: openFileExplorer({ sessionId, onSelect(path, from, to) }) let _feState = null; function openFileExplorer(opts = {}) { if (_feState) closeFileExplorer(); _feState = { sessionId: opts.sessionId || getCurrentSessionId(), onSelect: opts.onSelect || null, currentPath: '', history: [], selectedFile: null }; const overlay = document.createElement('div'); overlay.id = 'feOverlay'; overlay.className = 'fe-overlay'; overlay.addEventListener('click', (e) => { if (e.target === overlay) closeFileExplorer(); }); const modal = document.createElement('div'); modal.className = 'fe-modal'; modal.innerHTML = `
/
Laden...
Datei auswählen
`; overlay.appendChild(modal); document.body.appendChild(overlay); document.getElementById('feBack').addEventListener('click', feGoBack); document.getElementById('feClose').addEventListener('click', closeFileExplorer); document.getElementById('feSelectBtn').addEventListener('click', feDoSelect); // ESC _feState._keyHandler = (e) => { if (e.key === 'Escape') closeFileExplorer(); }; document.addEventListener('keydown', _feState._keyHandler); feLoadDir(''); } function closeFileExplorer() { const overlay = document.getElementById('feOverlay'); if (overlay) overlay.remove(); if (_feState && _feState._keyHandler) { document.removeEventListener('keydown', _feState._keyHandler); } _feState = null; } async function feLoadDir(path) { if (!_feState) return; _feState.currentPath = path; _feState.selectedFile = null; const pathEl = document.getElementById('fePath'); pathEl.textContent = '/' + (path || ''); const backBtn = document.getElementById('feBack'); backBtn.disabled = _feState.history.length === 0 && path === ''; const sidebar = document.getElementById('feSidebar'); sidebar.innerHTML = '
Laden...
'; // Preview zurücksetzen const main = document.getElementById('feMain'); main.innerHTML = '
Datei auswählen
'; document.getElementById('feFooter').hidden = true; try { const resp = await fetch(`/api/workspace/tree?sessionId=${_feState.sessionId}&path=${encodeURIComponent(path)}`); if (!resp.ok) { const err = await resp.json().catch(() => ({})); sidebar.innerHTML = `
${err.error || 'Fehler'}
`; return; } const entries = await resp.json(); sidebar.innerHTML = ''; if (path) { const upItem = document.createElement('div'); upItem.className = 'fe-item fe-dir'; upItem.textContent = '📁 ..'; upItem.addEventListener('click', () => { _feState.history.push(path); const parent = path.includes('/') ? path.substring(0, path.lastIndexOf('/')) : ''; feLoadDir(parent); }); sidebar.appendChild(upItem); } for (const entry of entries) { const item = document.createElement('div'); item.className = 'fe-item'; if (entry.type === 'dir') { item.classList.add('fe-dir'); item.textContent = '📁 ' + entry.name; item.addEventListener('click', () => { _feState.history.push(path); feLoadDir(entry.path); }); } else { item.classList.add('fe-file'); const icon = entry.isText ? '📄' : getFileIcon(entry.ext); item.textContent = icon + ' ' + entry.name; item.dataset.path = entry.path; item.dataset.isText = entry.isText; item.addEventListener('click', () => feSelectFile(entry, item)); } sidebar.appendChild(item); } if (entries.length === 0) { sidebar.innerHTML = '
Leer
'; } backBtn.disabled = path === ''; } catch (e) { sidebar.innerHTML = '
Netzwerk-Fehler
'; } } function feGoBack() { if (!_feState || _feState.history.length === 0) { if (_feState && _feState.currentPath) feLoadDir(''); return; } const prev = _feState.history.pop(); feLoadDir(prev); } async function feSelectFile(entry, itemEl) { if (!_feState) return; // Active-State setzen document.querySelectorAll('.fe-item.active').forEach(el => el.classList.remove('active')); itemEl.classList.add('active'); const main = document.getElementById('feMain'); const footer = document.getElementById('feFooter'); const info = document.getElementById('feInfo'); if (!entry.isText) { main.innerHTML = `
${getFileIcon(entry.ext)} ${entry.name}
${formatSize(entry.size)} - Vorschau nicht verfügbar
`; footer.hidden = true; _feState.selectedFile = null; return; } main.innerHTML = '
Laden...
'; try { const resp = await fetch(`/api/workspace/file?sessionId=${_feState.sessionId}&path=${encodeURIComponent(entry.path)}`); if (!resp.ok) { main.innerHTML = '
Fehler beim Laden
'; return; } const data = await resp.json(); _feState.selectedFile = data; // Textarea mit Zeilennummern main.innerHTML = `
`; const textarea = document.getElementById('feTextarea'); const lineNums = document.getElementById('feLineNums'); // Zeilennummern generieren renderLineNumbers(data.content, lineNums); // Sync scroll textarea.addEventListener('scroll', () => { lineNums.scrollTop = textarea.scrollTop; }); // Selection-Tracking für Zeilen-Info textarea.addEventListener('mouseup', () => updateSelectionInfo()); textarea.addEventListener('keyup', () => updateSelectionInfo()); info.textContent = `${data.path} - ${data.lineCount} Zeilen, ${formatSize(data.size)}`; footer.hidden = false; } catch (e) { main.innerHTML = '
Netzwerk-Fehler
'; } } function renderLineNumbers(content, container) { const lines = content.split('\n'); container.innerHTML = lines.map((_, i) => `
${i + 1}
`).join(''); } function updateSelectionInfo() { if (!_feState || !_feState.selectedFile) return; const textarea = document.getElementById('feTextarea'); const info = document.getElementById('feInfo'); const data = _feState.selectedFile; if (textarea.selectionStart !== textarea.selectionEnd) { const from = getLineNumber(textarea.value, textarea.selectionStart); const to = getLineNumber(textarea.value, textarea.selectionEnd); info.textContent = `${data.path}:${from}-${to} (${to - from + 1} Zeilen ausgewählt)`; } else { info.textContent = `${data.path} - ${data.lineCount} Zeilen, ${formatSize(data.size)}`; } } function getLineNumber(text, pos) { return text.substring(0, pos).split('\n').length; } function feDoSelect() { if (!_feState || !_feState.selectedFile) return; const data = _feState.selectedFile; const textarea = document.getElementById('feTextarea'); let ref = data.path; if (textarea && textarea.selectionStart !== textarea.selectionEnd) { const from = getLineNumber(textarea.value, textarea.selectionStart); const to = getLineNumber(textarea.value, textarea.selectionEnd); ref = `${data.path}:${from}-${to}`; } if (_feState.onSelect) { _feState.onSelect(ref); } closeFileExplorer(); } // ── Helpers ── function getFileIcon(ext) { const icons = { '.png': '🖼️', '.jpg': '🖼️', '.jpeg': '🖼️', '.gif': '🖼️', '.webp': '🖼️', '.svg': '🖼️', '.pdf': '📑', '.zip': '📦', '.gz': '📦', '.tar': '📦', '.mp3': '🎵', '.wav': '🎵', '.mp4': '🎬', '.avi': '🎬', '.exe': '⚙️', '.dll': '⚙️', '.so': '⚙️', '.db': '🗃️', '.sqlite': '🗃️' }; return icons[ext?.toLowerCase()] || '📄'; } function formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } function escHtml(s) { return s.replace(/&/g, '&').replace(//g, '>'); }