Files
gott/wwwroot/js/file-explorer.js
T
Meleki 76341dc42b Fix: Chat scrollt nach Font-Loading nochmal nach unten
scrollDown() wartet jetzt auf document.fonts.ready und scrollt erneut.
chat-page.js wartet nach loadHistory() zusätzlich per requestAnimationFrame
auf den Repaint nach Font-Swap.
2026-02-28 15:54:47 +01:00

507 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── 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,
initialFile: opts.initialFile || null // z.B. "src/foo.cs" – Datei direkt öffnen
};
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 = `
<div class="fe-header">
<div class="fe-nav">
<button class="fe-btn" id="feBack" title="Zurück" disabled>←</button>
<span class="fe-path" id="fePath">/</span>
</div>
<button class="fe-btn" id="feClose">✕</button>
</div>
<div class="fe-body">
<div class="fe-sidebar" id="feSidebar">
<div class="fe-loading">Laden...</div>
</div>
<div class="fe-main" id="feMain">
<div class="fe-placeholder">Datei auswählen</div>
</div>
</div>
<div class="fe-footer" id="feFooter" hidden>
<span class="fe-info" id="feInfo"></span>
<button class="fe-btn fe-select-btn" id="feSelectBtn">Einfügen</button>
</div>
`;
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);
// Zum Verzeichnis der initialFile navigieren, oder Root
if (_feState.initialFile) {
const filePath = _feState.initialFile;
const dir = filePath.includes('/') ? filePath.substring(0, filePath.lastIndexOf('/')) : '';
feLoadDir(dir);
} else {
feLoadDir('');
}
}
function closeFileExplorer() {
feDestroyEditor();
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 = '<div class="fe-loading">Laden...</div>';
// Preview zurücksetzen
feDestroyEditor();
const main = document.getElementById('feMain');
main.innerHTML = '<div class="fe-placeholder">Datei auswählen</div>';
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 = `<div class="fe-error">${err.error || 'Fehler'}</div>`;
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 = '<div class="fe-empty">Leer</div>';
}
backBtn.disabled = path === '';
// Wenn initialFile gesetzt, Datei automatisch selektieren
if (_feState && _feState.initialFile) {
const targetPath = _feState.initialFile;
_feState.initialFile = null; // Nur einmal triggern
// data-path Selektor sicher aufbauen
const allFiles = sidebar.querySelectorAll('.fe-file');
let targetItem = null;
allFiles.forEach(el => { if (el.dataset.path === targetPath) targetItem = el; });
if (targetItem) {
const entry = entries.find(e => e.path === targetPath);
if (entry) {
feSelectFile(entry, targetItem);
}
}
}
} catch (e) {
sidebar.innerHTML = '<div class="fe-error">Netzwerk-Fehler</div>';
}
}
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');
// Vorherige Editor-Instanz aufräumen
feDestroyEditor();
if (!entry.isText) {
main.innerHTML = `<div class="fe-placeholder">${getFileIcon(entry.ext)} ${entry.name}<br><small>${formatSize(entry.size)} - Vorschau nicht verfügbar</small></div>`;
footer.hidden = true;
_feState.selectedFile = null;
return;
}
main.innerHTML = '<div class="fe-loading">Laden...</div>';
try {
const resp = await fetch(`/api/workspace/file?sessionId=${_feState.sessionId}&path=${encodeURIComponent(entry.path)}`);
if (!resp.ok) {
main.innerHTML = '<div class="fe-error">Fehler beim Laden</div>';
return;
}
const data = await resp.json();
_feState.selectedFile = data;
const isMarkdown = entry.ext === '.md' || entry.ext === '.markdown';
if (isMarkdown) {
feInitEasyMDE(main, data);
} else {
feInitCodeMirror(main, data, entry.ext);
}
info.textContent = `${data.path} — ${data.lineCount} Zeilen, ${formatSize(data.size)}`;
footer.hidden = false;
} catch (e) {
main.innerHTML = '<div class="fe-error">Netzwerk-Fehler</div>';
}
}
function feDestroyEditor() {
if (_feState && _feState._editor) {
if (_feState._editorType === 'easymde') {
try { _feState._editor.toTextArea(); } catch (_) {}
}
// CodeMirror wird durch innerHTML-Reset automatisch verworfen
_feState._editor = null;
_feState._editorType = null;
}
}
function feInitCodeMirror(container, data, ext) {
container.innerHTML = '<div class="fe-cm-wrap"><textarea id="feCmTextarea"></textarea></div>';
const textarea = document.getElementById('feCmTextarea');
textarea.value = data.content;
const mode = getCmMode(ext);
const cm = CodeMirror.fromTextArea(textarea, {
mode: mode,
theme: 'default',
lineNumbers: true,
readOnly: true,
lineWrapping: false,
tabSize: 4,
indentWithTabs: false,
extraKeys: {}
});
// Selektion-Tracking für Zeilen-Info
cm.on('cursorActivity', () => {
if (!_feState || !_feState.selectedFile) return;
feUpdateSelectionInfo(cm, data);
});
_feState._editor = cm;
_feState._editorType = 'codemirror';
}
function feInitEasyMDE(container, data) {
container.innerHTML = '<div class="fe-mde-wrap"><textarea id="feMdeTextarea"></textarea></div>';
const textarea = document.getElementById('feMdeTextarea');
const mde = new EasyMDE({
element: textarea,
initialValue: data.content,
toolbar: false,
status: false,
autofocus: false,
spellChecker: false,
autoDownloadFontAwesome: false,
minHeight: '100%',
maxHeight: '100%'
});
// Readonly via CodeMirror-Instanz
mde.codemirror.setOption('readOnly', true);
mde.codemirror.setOption('theme', 'default');
mde.codemirror.on('cursorActivity', () => {
if (!_feState || !_feState.selectedFile) return;
feUpdateSelectionInfo(mde.codemirror, data);
});
_feState._editor = mde;
_feState._editorType = 'easymde';
}
function getCmMode(ext) {
const map = {
'.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
'.ts': 'javascript', '.jsx': 'javascript', '.tsx': 'javascript',
'.json': 'javascript',
'.css': 'css', '.scss': 'css', '.less': 'css',
'.html': 'htmlmixed', '.htm': 'htmlmixed',
'.xml': 'xml', '.svg': 'xml', '.csproj': 'xml', '.sln': 'xml',
'.py': 'python',
'.sh': 'shell', '.bash': 'shell', '.zsh': 'shell',
'.yaml': 'yaml', '.yml': 'yaml',
'.cs': 'clike', '.java': 'clike', '.cpp': 'clike', '.c': 'clike', '.h': 'clike'
};
return map[ext?.toLowerCase()] || 'null';
}
function feGetCm() {
if (!_feState || !_feState._editor) return null;
if (_feState._editorType === 'easymde') return _feState._editor.codemirror;
if (_feState._editorType === 'codemirror') return _feState._editor;
return null;
}
function feGetSelection(cm) {
const sel = cm.listSelections()[0];
const anchorLine = sel.anchor.line;
const headLine = sel.head.line;
const anchorCh = sel.anchor.ch;
const headCh = sel.head.ch;
// Echte Selektion: unterschiedliche Position (line oder ch)
const hasSelection = anchorLine !== headLine || anchorCh !== headCh;
if (!hasSelection) return null;
const fromLine = Math.min(anchorLine, headLine) + 1;
const toLine = Math.max(anchorLine, headLine) + 1;
return { from: fromLine, to: toLine };
}
function feUpdateSelectionInfo(cm, data) {
const info = document.getElementById('feInfo');
if (!info) return;
const sel = feGetSelection(cm);
if (sel && sel.from !== sel.to) {
info.textContent = `${data.path}:${sel.from}-${sel.to} (${sel.to - sel.from + 1} Zeilen ausgewählt)`;
} else if (sel) {
info.textContent = `${data.path}:${sel.from} (1 Zeile ausgewählt)`;
} else {
info.textContent = `${data.path} — ${data.lineCount} Zeilen, ${formatSize(data.size)}`;
}
}
function feDoSelect() {
if (!_feState || !_feState.selectedFile) return;
const data = _feState.selectedFile;
let ref = data.path;
const cm = feGetCm();
if (cm) {
const sel = feGetSelection(cm);
if (sel) {
ref = sel.from === sel.to
? `${data.path}:${sel.from}`
: `${data.path}:${sel.from}-${sel.to}`;
}
}
if (_feState.onSelect) {
_feState.onSelect(ref);
}
closeFileExplorer();
}
// ── Folder Browser Mode ──
// Nutzt /api/workspace/browse mit absolutem Pfad, zum Ordner-Auswählen.
function openFolderBrowser(opts = {}) {
if (_feState) closeFileExplorer();
_feState = {
mode: 'folder',
onSelect: opts.onSelect || null,
currentAbsPath: opts.startPath || '/',
history: []
};
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 = `
<div class="fe-header">
<div class="fe-nav">
<button class="fe-btn" id="feBack" title="Zurück" disabled>←</button>
<span class="fe-path" id="fePath">/</span>
</div>
<button class="fe-btn" id="feClose">✕</button>
</div>
<div class="fe-body">
<div class="fe-sidebar fe-sidebar-full" id="feSidebar">
<div class="fe-loading">Laden...</div>
</div>
</div>
<div class="fe-footer" id="feFooter">
<span class="fe-info" id="feInfo"></span>
<button class="fe-btn fe-select-btn" id="feSelectBtn">Diesen Ordner wählen</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
document.getElementById('feBack').addEventListener('click', fbGoBack);
document.getElementById('feClose').addEventListener('click', closeFileExplorer);
document.getElementById('feSelectBtn').addEventListener('click', () => {
if (_feState && _feState.onSelect) _feState.onSelect(_feState.currentAbsPath);
closeFileExplorer();
});
_feState._keyHandler = (e) => { if (e.key === 'Escape') closeFileExplorer(); };
document.addEventListener('keydown', _feState._keyHandler);
fbLoadDir(opts.startPath || '/');
}
async function fbLoadDir(absPath) {
if (!_feState) return;
const pathEl = document.getElementById('fePath');
const sidebar = document.getElementById('feSidebar');
const info = document.getElementById('feInfo');
pathEl.textContent = absPath;
info.textContent = absPath;
sidebar.innerHTML = '<div class="fe-loading">Laden...</div>';
try {
const resp = await fetch(`/api/workspace/browse?path=${encodeURIComponent(absPath)}`);
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
sidebar.innerHTML = `<div class="fe-error">${err.error || 'Fehler'}</div>`;
return;
}
const data = await resp.json();
_feState.currentAbsPath = data.current;
pathEl.textContent = data.current;
info.textContent = data.current;
sidebar.innerHTML = '';
for (const entry of data.entries) {
const item = document.createElement('div');
item.className = 'fe-item fe-dir';
item.textContent = '📁 ' + entry.name;
item.addEventListener('click', () => {
_feState.history.push(_feState.currentAbsPath);
fbLoadDir(entry.path);
});
sidebar.appendChild(item);
}
if (data.entries.length === 0) {
sidebar.innerHTML = '<div class="fe-empty">Leer</div>';
}
document.getElementById('feBack').disabled = _feState.history.length === 0;
} catch (e) {
sidebar.innerHTML = '<div class="fe-error">Netzwerk-Fehler</div>';
}
}
function fbGoBack() {
if (!_feState || _feState.history.length === 0) return;
const prev = _feState.history.pop();
fbLoadDir(prev);
}
// ── 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}