Pfade werden auf Windows nativ mit Backslashes zurückgegeben statt zu Forward-Slashes konvertiert. Navigation von Laufwerk (C:) zurück zur Laufwerksliste ermöglicht. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
729 lines
24 KiB
JavaScript
729 lines
24 KiB
JavaScript
// ── File Explorer Modal ──
|
||
// Wiederverwendbares Modal zum Durchsuchen des Workspaces.
|
||
// Nutzung: openFileExplorer({ sessionId, onSelect(path, from, to) })
|
||
|
||
let _feState = null;
|
||
|
||
function _feApiUrl(action, params = '') {
|
||
if (_feState && _feState.agentId) {
|
||
return `/api/workspace/agent/${action}?agentId=${_feState.agentId}${params ? '&' + params : ''}`;
|
||
}
|
||
return `/api/workspace/${action}?sessionId=${_feState.sessionId}${params ? '&' + params : ''}`;
|
||
}
|
||
|
||
function openFileExplorer(opts = {}) {
|
||
if (_feState) closeFileExplorer();
|
||
|
||
_feState = {
|
||
sessionId: opts.sessionId || (!opts.agentId ? getCurrentSessionId() : null),
|
||
agentId: opts.agentId || null,
|
||
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 icon-btn" id="feBack" title="Zurück" disabled><i class="fa fa-arrow-left"></i></button>
|
||
<span class="fe-path" id="fePath">/</span>
|
||
</div>
|
||
<button class="fe-btn icon-btn" id="feNewDir" title="Neuer Ordner"><i class="fa fa-folder-open"></i> <i class="fa fa-plus"></i></button>
|
||
<button class="fe-btn icon-btn" id="feClose"><i class="fa fa-times"></i></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>
|
||
<div class="fe-footer-actions">
|
||
<button class="fe-btn fe-save-btn" id="feSaveBtn" disabled hidden>Speichern</button>
|
||
<button class="fe-btn fe-select-btn" id="feSelectBtn">Einfügen</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
overlay.appendChild(modal);
|
||
document.body.appendChild(overlay);
|
||
|
||
document.getElementById('feBack').addEventListener('click', feGoBack);
|
||
document.getElementById('feNewDir').addEventListener('click', feCreateDir);
|
||
document.getElementById('feClose').addEventListener('click', closeFileExplorer);
|
||
document.getElementById('feSelectBtn').addEventListener('click', feDoSelect);
|
||
document.getElementById('feSaveBtn').addEventListener('click', feDoSave);
|
||
|
||
// 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(_feApiUrl('tree', `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.innerHTML = '<i class="fa fa-folder"></i> ..';
|
||
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');
|
||
const label = document.createElement('span');
|
||
label.className = 'fe-dir-label';
|
||
label.innerHTML = '<i class="fa fa-folder"></i> ' + esc(entry.name);
|
||
label.addEventListener('click', () => {
|
||
_feState.history.push(path);
|
||
feLoadDir(entry.path);
|
||
});
|
||
item.appendChild(label);
|
||
|
||
const delBtn = document.createElement('button');
|
||
delBtn.className = 'fe-btn fe-dir-del';
|
||
delBtn.title = 'Ordner löschen (nur wenn leer)';
|
||
delBtn.innerHTML = '<i class="fa fa-trash-o"></i>';
|
||
delBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
feDeleteDir(entry.path, entry.name);
|
||
});
|
||
item.appendChild(delBtn);
|
||
} else {
|
||
item.classList.add('fe-file');
|
||
const icon = entry.isText ? '<i class="fa fa-file-text-o"></i>' : getFileIcon(entry.ext);
|
||
item.innerHTML = icon + ' ' + esc(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();
|
||
|
||
const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico'];
|
||
if (!entry.isText && imageExts.includes((entry.ext || '').toLowerCase())) {
|
||
const rawUrl = _feApiUrl('raw', `path=${encodeURIComponent(entry.path)}`);
|
||
main.innerHTML = `<div class="fe-image-preview"><img src="${rawUrl}" alt="${esc(entry.name)}" /></div>`;
|
||
footer.hidden = true;
|
||
_feState.selectedFile = null;
|
||
return;
|
||
}
|
||
|
||
if (!entry.isText) {
|
||
main.innerHTML = `<div class="fe-placeholder">${getFileIcon(entry.ext)} ${esc(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(_feApiUrl('file', `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)}`;
|
||
|
||
const saveBtn = document.getElementById('feSaveBtn');
|
||
const selectBtn = document.getElementById('feSelectBtn');
|
||
saveBtn.hidden = false;
|
||
saveBtn.disabled = true;
|
||
selectBtn.hidden = !_feState.onSelect;
|
||
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"><div id="feCmHost"></div></div>';
|
||
|
||
const host = document.getElementById('feCmHost');
|
||
const mode = getCmMode(ext);
|
||
_feState._originalContent = data.content;
|
||
|
||
const cm = CodeMirror(host, {
|
||
value: data.content,
|
||
mode: mode,
|
||
theme: 'default',
|
||
lineNumbers: true,
|
||
readOnly: false,
|
||
lineWrapping: false,
|
||
tabSize: 4,
|
||
indentWithTabs: false,
|
||
extraKeys: {
|
||
'Ctrl-S': () => feDoSave(),
|
||
'Cmd-S': () => feDoSave()
|
||
}
|
||
});
|
||
|
||
// Selektion-Tracking für Zeilen-Info
|
||
cm.on('cursorActivity', () => {
|
||
if (!_feState || !_feState.selectedFile) return;
|
||
feUpdateSelectionInfo(cm, data);
|
||
});
|
||
|
||
// Änderungs-Tracking
|
||
cm.on('change', () => {
|
||
const saveBtn = document.getElementById('feSaveBtn');
|
||
if (saveBtn) saveBtn.disabled = cm.getValue() === _feState._originalContent;
|
||
});
|
||
|
||
// Refresh nötig wenn CM in einem Container ohne finale Größe initialisiert wird
|
||
requestAnimationFrame(() => cm.refresh());
|
||
|
||
_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');
|
||
_feState._originalContent = data.content;
|
||
|
||
const mde = createMDE(textarea, {
|
||
initialValue: data.content,
|
||
toolbar: ['bold', 'italic', 'heading', '|', 'quote', 'unordered-list', 'ordered-list', '|', 'link', 'image', 'code', '|', 'preview', 'side-by-side', '|', 'guide'],
|
||
minHeight: '100%',
|
||
maxHeight: '100%'
|
||
});
|
||
|
||
mde.codemirror.setOption('extraKeys', {
|
||
'Ctrl-S': () => feDoSave(),
|
||
'Cmd-S': () => feDoSave()
|
||
});
|
||
|
||
mde.codemirror.on('cursorActivity', () => {
|
||
if (!_feState || !_feState.selectedFile) return;
|
||
feUpdateSelectionInfo(mde.codemirror, data);
|
||
});
|
||
|
||
mde.codemirror.on('change', () => {
|
||
const saveBtn = document.getElementById('feSaveBtn');
|
||
if (saveBtn) saveBtn.disabled = mde.value() === _feState._originalContent;
|
||
});
|
||
|
||
_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();
|
||
}
|
||
|
||
async function feDoSave() {
|
||
if (!_feState || !_feState.selectedFile) return;
|
||
|
||
const cm = feGetCm();
|
||
if (!cm) return;
|
||
|
||
const saveBtn = document.getElementById('feSaveBtn');
|
||
if (saveBtn && saveBtn.disabled) return;
|
||
|
||
const content = cm.getValue();
|
||
const path = _feState.selectedFile.path;
|
||
|
||
if (saveBtn) {
|
||
saveBtn.disabled = true;
|
||
saveBtn.textContent = 'Speichern...';
|
||
}
|
||
|
||
try {
|
||
const resp = await fetch(
|
||
_feApiUrl('file', `path=${encodeURIComponent(path)}`),
|
||
{ method: 'PUT', body: content }
|
||
);
|
||
|
||
if (!resp.ok) {
|
||
const err = await resp.json().catch(() => ({}));
|
||
alert('Fehler beim Speichern: ' + (err.error || resp.status));
|
||
if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; }
|
||
return;
|
||
}
|
||
|
||
const result = await resp.json();
|
||
|
||
// State aktualisieren
|
||
_feState.selectedFile.content = content;
|
||
_feState.selectedFile.size = result.size;
|
||
_feState.selectedFile.lineCount = result.lineCount;
|
||
_feState._originalContent = content;
|
||
|
||
const info = document.getElementById('feInfo');
|
||
if (info) info.textContent = `${path} — ${result.lineCount} Zeilen, ${formatSize(result.size)}`;
|
||
|
||
if (saveBtn) { saveBtn.textContent = 'Speichern'; }
|
||
// bleibt disabled, da gespeicherter Stand == aktueller Stand
|
||
|
||
} catch (e) {
|
||
alert('Netzwerk-Fehler beim Speichern');
|
||
if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; }
|
||
}
|
||
}
|
||
|
||
// ── 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 || '/',
|
||
basePath: (opts.basePath || '').replace(/\/+$/, '') || null,
|
||
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 icon-btn" id="feBack" title="Zurück" disabled><i class="fa fa-arrow-left"></i></button>
|
||
<span class="fe-path" id="fePath">/</span>
|
||
</div>
|
||
<button class="fe-btn icon-btn" id="feNewDir" title="Neuer Ordner"><i class="fa fa-folder-open"></i> <i class="fa fa-plus"></i></button>
|
||
<button class="fe-btn icon-btn" id="feClose"><i class="fa fa-times"></i></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('feNewDir').addEventListener('click', fbCreateDir);
|
||
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 || (serverIsWindows ? '' : '/'));
|
||
}
|
||
|
||
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.innerHTML = '<i class="fa fa-folder"></i> ' + esc(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>';
|
||
}
|
||
|
||
const currentNorm = data.current.replace(/[\\/]+$/, '');
|
||
const atBase = _feState.basePath && currentNorm === _feState.basePath;
|
||
const atDriveList = serverIsWindows && currentNorm === '';
|
||
const atRoot = currentNorm === '' || currentNorm === '/';
|
||
document.getElementById('feBack').disabled = atBase || (atRoot && _feState.history.length === 0) || atDriveList;
|
||
} catch (e) {
|
||
sidebar.innerHTML = '<div class="fe-error">Netzwerk-Fehler</div>';
|
||
}
|
||
}
|
||
|
||
function fbGoBack() {
|
||
if (!_feState) return;
|
||
// Nicht über den basePath hinaus navigieren
|
||
const currentNorm = _feState.currentAbsPath.replace(/[\\/]+$/, '');
|
||
if (_feState.basePath && currentNorm === _feState.basePath) return;
|
||
|
||
if (_feState.history.length > 0) {
|
||
const prev = _feState.history.pop();
|
||
// Prüfe ob der vorherige Pfad über dem basePath liegt
|
||
if (_feState.basePath && !prev.replace(/[\\/]+$/, '').startsWith(_feState.basePath)) {
|
||
_feState.history.length = 0;
|
||
fbLoadDir(_feState.basePath);
|
||
return;
|
||
}
|
||
fbLoadDir(prev);
|
||
} else {
|
||
// Kein History-Eintrag → zum Parent navigieren
|
||
let parent;
|
||
if (serverIsWindows && /^[A-Z]:$/i.test(currentNorm)) {
|
||
parent = ''; // Laufwerk → zurück zur Laufwerksauswahl
|
||
} else if (serverIsWindows && /^[A-Z]:[\\\/][^\\\/]*$/i.test(currentNorm)) {
|
||
parent = currentNorm.substring(0, 2); // z.B. C:\Users → C:
|
||
} else {
|
||
const lastSep = Math.max(currentNorm.lastIndexOf('/'), currentNorm.lastIndexOf('\\'));
|
||
parent = lastSep > 0 ? currentNorm.substring(0, lastSep) : (serverIsWindows ? '' : '/');
|
||
}
|
||
if (parent !== currentNorm) {
|
||
fbLoadDir(parent);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Folder Browser: Create Directory ──
|
||
|
||
async function fbCreateDir() {
|
||
if (!_feState) return;
|
||
|
||
const name = prompt('Ordnername:');
|
||
if (!name || !name.trim()) return;
|
||
|
||
const dirName = name.trim();
|
||
if (/[\/\\:*?"<>|]/.test(dirName)) {
|
||
alert('Ungültiger Ordnername');
|
||
return;
|
||
}
|
||
|
||
const absPath = _feState.currentAbsPath.replace(/\/$/, '') + '/' + dirName;
|
||
|
||
try {
|
||
const resp = await fetch(`/api/workspace/browse/mkdir?path=${encodeURIComponent(absPath)}`, { method: 'POST' });
|
||
if (!resp.ok) {
|
||
const err = await resp.json().catch(() => ({}));
|
||
alert(err.error || 'Fehler beim Erstellen');
|
||
return;
|
||
}
|
||
fbLoadDir(_feState.currentAbsPath);
|
||
} catch (e) {
|
||
alert('Netzwerk-Fehler');
|
||
}
|
||
}
|
||
|
||
// ── File Explorer: Create / Delete Directory ──
|
||
|
||
async function feCreateDir() {
|
||
if (!_feState) return;
|
||
|
||
const name = prompt('Ordnername:');
|
||
if (!name || !name.trim()) return;
|
||
|
||
const dirName = name.trim();
|
||
if (/[\/\\:*?"<>|]/.test(dirName)) {
|
||
alert('Ungültiger Ordnername');
|
||
return;
|
||
}
|
||
|
||
const newPath = _feState.currentPath ? _feState.currentPath + '/' + dirName : dirName;
|
||
|
||
try {
|
||
const url = _feApiUrl('mkdir', `path=${encodeURIComponent(newPath)}`);
|
||
const resp = await fetch(url, { method: 'POST' });
|
||
if (!resp.ok) {
|
||
const err = await resp.json().catch(() => ({}));
|
||
alert(err.error || 'Fehler beim Erstellen');
|
||
return;
|
||
}
|
||
feLoadDir(_feState.currentPath);
|
||
} catch (e) {
|
||
alert('Netzwerk-Fehler');
|
||
}
|
||
}
|
||
|
||
async function feDeleteDir(dirPath, dirName) {
|
||
if (!_feState) return;
|
||
if (!confirm(`Ordner "${dirName}" löschen?\n(Nur möglich wenn leer)`)) return;
|
||
|
||
try {
|
||
const url = _feApiUrl('dir', `path=${encodeURIComponent(dirPath)}`);
|
||
const resp = await fetch(url, { method: 'DELETE' });
|
||
if (!resp.ok) {
|
||
const err = await resp.json().catch(() => ({}));
|
||
alert(err.error || 'Fehler beim Löschen');
|
||
return;
|
||
}
|
||
feLoadDir(_feState.currentPath);
|
||
} catch (e) {
|
||
alert('Netzwerk-Fehler');
|
||
}
|
||
}
|
||
|
||
// ── Helpers ──
|
||
|
||
function getFileIcon(ext) {
|
||
const icons = {
|
||
'.png': '<i class="fa fa-image"></i>', '.jpg': '<i class="fa fa-image"></i>',
|
||
'.jpeg': '<i class="fa fa-image"></i>', '.gif': '<i class="fa fa-image"></i>',
|
||
'.webp': '<i class="fa fa-image"></i>', '.svg': '<i class="fa fa-image"></i>',
|
||
'.pdf': '<i class="fa fa-file-pdf-o"></i>',
|
||
'.zip': '<i class="fa fa-archive"></i>', '.gz': '<i class="fa fa-archive"></i>',
|
||
'.tar': '<i class="fa fa-archive"></i>',
|
||
'.mp3': '<i class="fa fa-file-audio-o"></i>', '.wav': '<i class="fa fa-file-audio-o"></i>',
|
||
'.mp4': '<i class="fa fa-file-video-o"></i>', '.avi': '<i class="fa fa-file-video-o"></i>',
|
||
'.exe': '<i class="fa fa-cog"></i>', '.dll': '<i class="fa fa-cog"></i>',
|
||
'.so': '<i class="fa fa-cog"></i>',
|
||
'.db': '<i class="fa fa-database"></i>', '.sqlite': '<i class="fa fa-database"></i>'
|
||
};
|
||
return icons[ext?.toLowerCase()] || '<i class="fa fa-file-text-o"></i>';
|
||
}
|
||
|
||
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, '<').replace(/>/g, '>');
|
||
}
|