Neue Features: - 3-Panel Dateimanager (Ordnerbaum, Dateiliste, Vorschau) - Separates Vorschau-Fenster für zweiten Monitor - Resize-Handles für flexible Panel-Größen (horizontal & vertikal) - Vorschau-Panel ausblendbar wenn externes Fenster aktiv - Natürliche Sortierung (Sonderzeichen → Zahlen → Buchstaben) - PDF-Vorschau mit Fit-to-Page - Email-Attachment Abruf erweitert Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1079 lines
34 KiB
JavaScript
1079 lines
34 KiB
JavaScript
/**
|
|
* Dateimanager / Browser JavaScript
|
|
* Dual-Pane Datei-Browser mit separatem Vorschau-Fenster
|
|
*/
|
|
|
|
// ============ State ============
|
|
let state = {
|
|
currentPath: '/srv/http/dateiverwaltung/data',
|
|
selectedFile: null,
|
|
selectedFilePath: null,
|
|
files: [],
|
|
folders: [],
|
|
modalBrowserPath: '/',
|
|
verschiebenPath: '/',
|
|
folderTree: {}, // Baum-Cache
|
|
expandedFolders: new Set(), // Aufgeklappte Ordner
|
|
previewWindow: null, // Referenz zum Vorschau-Fenster
|
|
previewWindowOpen: false,
|
|
previewPanelHidden: false, // Preview-Panel ausgeblendet
|
|
isVerticalMode: false // Vertikaler Layout-Modus
|
|
};
|
|
|
|
// ============ BroadcastChannel für Vorschau-Fenster ============
|
|
const previewChannel = new BroadcastChannel('dateiverwaltung-preview');
|
|
|
|
previewChannel.onmessage = (event) => {
|
|
const { type } = event.data;
|
|
|
|
switch (type) {
|
|
case 'preview-window-ready':
|
|
state.previewWindowOpen = true;
|
|
aktualisierePreviewButton();
|
|
aktualisiereHidePreviewButton();
|
|
// Aktuelle Datei an Vorschau senden
|
|
if (state.selectedFilePath) {
|
|
sendeAnVorschau(state.selectedFilePath, state.selectedFile);
|
|
}
|
|
break;
|
|
case 'preview-window-closed':
|
|
state.previewWindowOpen = false;
|
|
state.previewWindow = null;
|
|
aktualisierePreviewButton();
|
|
aktualisiereHidePreviewButton();
|
|
break;
|
|
case 'pong':
|
|
state.previewWindowOpen = true;
|
|
aktualisierePreviewButton();
|
|
aktualisiereHidePreviewButton();
|
|
break;
|
|
}
|
|
};
|
|
|
|
function sendeAnVorschau(pfad, name) {
|
|
previewChannel.postMessage({
|
|
type: 'preview',
|
|
data: { path: pfad, name: name }
|
|
});
|
|
}
|
|
|
|
function aktualisierePreviewButton() {
|
|
const btn = document.getElementById('btn-open-preview');
|
|
if (btn) {
|
|
if (state.previewWindowOpen) {
|
|
btn.textContent = '🖥️ Vorschau-Fenster aktiv';
|
|
btn.classList.add('active');
|
|
} else {
|
|
btn.textContent = '🖥️ Vorschau-Fenster öffnen';
|
|
btn.classList.remove('active');
|
|
}
|
|
}
|
|
}
|
|
|
|
function aktualisiereHidePreviewButton() {
|
|
const btnHide = document.getElementById('btn-hide-preview');
|
|
const btnShow = document.getElementById('btn-show-preview');
|
|
|
|
// "Ausblenden" Button nur zeigen wenn Preview-Fenster aktiv
|
|
if (btnHide) {
|
|
if (state.previewWindowOpen && !state.previewPanelHidden) {
|
|
btnHide.classList.remove('hidden');
|
|
} else {
|
|
btnHide.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
// "Einblenden" Button zeigen wenn Panel ausgeblendet
|
|
if (btnShow) {
|
|
if (state.previewPanelHidden) {
|
|
btnShow.classList.remove('hidden');
|
|
} else {
|
|
btnShow.classList.add('hidden');
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============ API Helper ============
|
|
async function api(endpoint, options = {}) {
|
|
const url = `/api${endpoint}`;
|
|
const config = {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
...options
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(url, config);
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('API Error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ============ Initialisierung ============
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
ladeTheme();
|
|
pruefeLayoutModus();
|
|
initResizeHandles();
|
|
|
|
// Gespeicherten Pfad laden
|
|
const gespeicherterPfad = localStorage.getItem('browser_path');
|
|
if (gespeicherterPfad) {
|
|
state.currentPath = gespeicherterPfad;
|
|
}
|
|
|
|
// Aufgeklappte Ordner laden
|
|
const gespeicherteExpanded = localStorage.getItem('browser_expanded');
|
|
if (gespeicherteExpanded) {
|
|
try {
|
|
state.expandedFolders = new Set(JSON.parse(gespeicherteExpanded));
|
|
} catch (e) {}
|
|
}
|
|
|
|
// Preview-Panel Status laden
|
|
const previewHidden = localStorage.getItem('browser_preview_hidden');
|
|
if (previewHidden === 'true') {
|
|
state.previewPanelHidden = true;
|
|
togglePreviewPanelUI(true);
|
|
}
|
|
|
|
// Baum initialisieren
|
|
ladeBaum('/');
|
|
ladeOrdnerInhalt(state.currentPath);
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', handleKeydown);
|
|
|
|
// Context menu schließen bei Klick außerhalb
|
|
document.addEventListener('click', () => {
|
|
const menu = document.querySelector('.context-menu');
|
|
if (menu) menu.remove();
|
|
});
|
|
|
|
// Prüfen ob Vorschau-Fenster bereits offen
|
|
previewChannel.postMessage({ type: 'ping' });
|
|
|
|
// Layout bei Resize prüfen
|
|
window.addEventListener('resize', pruefeLayoutModus);
|
|
});
|
|
|
|
// ============ Layout Modus ============
|
|
function pruefeLayoutModus() {
|
|
const istVertical = window.innerWidth <= 1000;
|
|
if (istVertical !== state.isVerticalMode) {
|
|
state.isVerticalMode = istVertical;
|
|
const main = document.getElementById('browser-main');
|
|
if (main) {
|
|
if (istVertical) {
|
|
main.classList.add('vertical');
|
|
} else {
|
|
main.classList.remove('vertical');
|
|
}
|
|
}
|
|
// Resize-Handles neu initialisieren
|
|
initResizeHandles();
|
|
}
|
|
}
|
|
|
|
// ============ Theme ============
|
|
function ladeTheme() {
|
|
const gespeichertesTheme = localStorage.getItem('theme') || 'auto';
|
|
wendeThemeAn(gespeichertesTheme);
|
|
document.getElementById('theme-select').value = gespeichertesTheme;
|
|
}
|
|
|
|
function wendeThemeAn(theme) {
|
|
const html = document.documentElement;
|
|
if (theme === 'auto') {
|
|
html.removeAttribute('data-theme');
|
|
} else {
|
|
html.setAttribute('data-theme', theme);
|
|
}
|
|
}
|
|
|
|
function wechsleTheme(theme) {
|
|
wendeThemeAn(theme);
|
|
localStorage.setItem('theme', theme);
|
|
}
|
|
|
|
// ============ Preview Panel Toggle ============
|
|
function togglePreviewPanel() {
|
|
state.previewPanelHidden = !state.previewPanelHidden;
|
|
togglePreviewPanelUI(state.previewPanelHidden);
|
|
localStorage.setItem('browser_preview_hidden', state.previewPanelHidden);
|
|
aktualisiereHidePreviewButton();
|
|
}
|
|
|
|
function togglePreviewPanelUI(hidden) {
|
|
const previewPane = document.getElementById('pane-preview');
|
|
const resizeHandle = document.getElementById('resize-handle-2');
|
|
const listPane = document.getElementById('pane-list');
|
|
|
|
if (previewPane) {
|
|
if (hidden) {
|
|
previewPane.classList.add('hidden-panel');
|
|
} else {
|
|
previewPane.classList.remove('hidden-panel');
|
|
}
|
|
}
|
|
|
|
if (resizeHandle) {
|
|
if (hidden) {
|
|
resizeHandle.classList.add('hidden-panel');
|
|
} else {
|
|
resizeHandle.classList.remove('hidden-panel');
|
|
}
|
|
}
|
|
|
|
// Dateiliste expandieren wenn Preview ausgeblendet
|
|
if (listPane) {
|
|
if (hidden) {
|
|
listPane.classList.add('expanded');
|
|
listPane.style.width = ''; // Inline-Style entfernen
|
|
} else {
|
|
listPane.classList.remove('expanded');
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============ Ordner-Baum ============
|
|
async function ladeBaum(startPfad) {
|
|
try {
|
|
const result = await api(`/browse?path=${encodeURIComponent(startPfad)}`);
|
|
if (result.error) return;
|
|
|
|
// Root-Knoten
|
|
if (startPfad === '/') {
|
|
state.folderTree['/'] = {
|
|
name: '/',
|
|
path: '/',
|
|
children: result.entries
|
|
.filter(e => e.type === 'directory')
|
|
.map(e => e.path)
|
|
};
|
|
}
|
|
|
|
// Unterordner cachen
|
|
result.entries.forEach(entry => {
|
|
if (entry.type === 'directory') {
|
|
if (!state.folderTree[entry.path]) {
|
|
state.folderTree[entry.path] = {
|
|
name: entry.name,
|
|
path: entry.path,
|
|
children: null, // Noch nicht geladen
|
|
loaded: false
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
aktualisiereBaumAnzeige();
|
|
} catch (e) {
|
|
console.error('Fehler beim Laden des Baums:', e);
|
|
}
|
|
}
|
|
|
|
async function ladeBaumKnoten(pfad) {
|
|
try {
|
|
const result = await api(`/browse?path=${encodeURIComponent(pfad)}`);
|
|
if (result.error) return;
|
|
|
|
const children = result.entries
|
|
.filter(e => e.type === 'directory')
|
|
.map(e => e.path);
|
|
|
|
state.folderTree[pfad] = {
|
|
...state.folderTree[pfad],
|
|
children: children,
|
|
loaded: true
|
|
};
|
|
|
|
// Kinder registrieren
|
|
result.entries.forEach(entry => {
|
|
if (entry.type === 'directory' && !state.folderTree[entry.path]) {
|
|
state.folderTree[entry.path] = {
|
|
name: entry.name,
|
|
path: entry.path,
|
|
children: null,
|
|
loaded: false
|
|
};
|
|
}
|
|
});
|
|
|
|
aktualisiereBaumAnzeige();
|
|
} catch (e) {
|
|
console.error('Fehler beim Laden des Knotens:', e);
|
|
}
|
|
}
|
|
|
|
function aktualisiereBaumAnzeige() {
|
|
const container = document.getElementById('folder-tree');
|
|
if (!container) return;
|
|
|
|
// Standard-Startordner
|
|
const startPfade = ['/', '/mnt', '/srv', '/home'];
|
|
let html = '';
|
|
|
|
startPfade.forEach(pfad => {
|
|
html += renderBaumKnoten(pfad, 0);
|
|
});
|
|
|
|
container.innerHTML = html || '<p class="empty-state">Keine Ordner</p>';
|
|
}
|
|
|
|
function renderBaumKnoten(pfad, tiefe) {
|
|
const knoten = state.folderTree[pfad];
|
|
if (!knoten) {
|
|
// Knoten existiert nicht im Cache - erstelle Platzhalter
|
|
return `
|
|
<div class="tree-item" style="padding-left: ${tiefe * 16}px" onclick="expandiereBaum('${pfad}')">
|
|
<span class="tree-toggle">▶</span>
|
|
<span class="tree-icon">📁</span>
|
|
<span class="tree-name">${pfad.split('/').pop() || pfad}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
const istExpanded = state.expandedFolders.has(pfad);
|
|
const istAktiv = state.currentPath === pfad || state.currentPath.startsWith(pfad + '/');
|
|
const hatKinder = knoten.children && knoten.children.length > 0;
|
|
const istGeladen = knoten.loaded;
|
|
|
|
let html = `
|
|
<div class="tree-item ${istAktiv ? 'active' : ''}"
|
|
style="padding-left: ${tiefe * 16 + 8}px"
|
|
onclick="waehleBaumOrdner('${pfad}')"
|
|
ondblclick="expandiereBaum('${pfad}')">
|
|
<span class="tree-toggle" onclick="event.stopPropagation(); toggleBaum('${pfad}')">
|
|
${hatKinder || !istGeladen ? (istExpanded ? '▼' : '▶') : ''}
|
|
</span>
|
|
<span class="tree-icon">${istExpanded ? '📂' : '📁'}</span>
|
|
<span class="tree-name">${knoten.name}</span>
|
|
</div>
|
|
`;
|
|
|
|
// Kinder rendern wenn aufgeklappt
|
|
if (istExpanded && knoten.children) {
|
|
knoten.children.forEach(childPfad => {
|
|
html += renderBaumKnoten(childPfad, tiefe + 1);
|
|
});
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
async function toggleBaum(pfad) {
|
|
if (state.expandedFolders.has(pfad)) {
|
|
state.expandedFolders.delete(pfad);
|
|
} else {
|
|
state.expandedFolders.add(pfad);
|
|
// Laden wenn noch nicht geladen
|
|
const knoten = state.folderTree[pfad];
|
|
if (!knoten || !knoten.loaded) {
|
|
await ladeBaumKnoten(pfad);
|
|
}
|
|
}
|
|
|
|
// Speichern
|
|
localStorage.setItem('browser_expanded', JSON.stringify([...state.expandedFolders]));
|
|
aktualisiereBaumAnzeige();
|
|
}
|
|
|
|
async function expandiereBaum(pfad) {
|
|
if (!state.expandedFolders.has(pfad)) {
|
|
state.expandedFolders.add(pfad);
|
|
const knoten = state.folderTree[pfad];
|
|
if (!knoten || !knoten.loaded) {
|
|
await ladeBaumKnoten(pfad);
|
|
}
|
|
localStorage.setItem('browser_expanded', JSON.stringify([...state.expandedFolders]));
|
|
aktualisiereBaumAnzeige();
|
|
}
|
|
}
|
|
|
|
function waehleBaumOrdner(pfad) {
|
|
navigiereZu(pfad);
|
|
}
|
|
|
|
// ============ Ordner Navigation ============
|
|
async function ladeOrdnerInhalt(pfad) {
|
|
try {
|
|
const result = await api(`/browse/files?path=${encodeURIComponent(pfad)}`);
|
|
|
|
if (result.error) {
|
|
toast(result.error, 'error');
|
|
return;
|
|
}
|
|
|
|
state.currentPath = result.current;
|
|
state.folders = result.folders || [];
|
|
state.files = result.files || [];
|
|
|
|
// Pfad speichern
|
|
localStorage.setItem('browser_path', state.currentPath);
|
|
|
|
// UI aktualisieren
|
|
aktualisiereBreadcrumb();
|
|
aktualisiereDateiliste();
|
|
aktualisiereBaumAnzeige();
|
|
|
|
// Vorschau zurücksetzen
|
|
if (state.selectedFile) {
|
|
const existiert = state.files.some(f => f.name === state.selectedFile);
|
|
if (!existiert) {
|
|
state.selectedFile = null;
|
|
state.selectedFilePath = null;
|
|
if (!state.previewWindowOpen && !state.previewPanelHidden) {
|
|
zeigeLeereVorschau();
|
|
}
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
toast('Fehler beim Laden: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
function aktualisiereBreadcrumb() {
|
|
const container = document.getElementById('breadcrumb');
|
|
const teile = state.currentPath.split('/').filter(t => t);
|
|
|
|
let html = `<span class="breadcrumb-item" onclick="navigiereZu('/')">/</span>`;
|
|
let pfad = '';
|
|
|
|
teile.forEach((teil, index) => {
|
|
pfad += '/' + teil;
|
|
const istLetzter = index === teile.length - 1;
|
|
|
|
if (istLetzter) {
|
|
html += `<span class="breadcrumb-separator">/</span>`;
|
|
html += `<span class="breadcrumb-current">${teil}</span>`;
|
|
} else {
|
|
html += `<span class="breadcrumb-separator">/</span>`;
|
|
html += `<span class="breadcrumb-item" onclick="navigiereZu('${pfad}')">${teil}</span>`;
|
|
}
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function aktualisiereDateiliste() {
|
|
const container = document.getElementById('file-list');
|
|
const countEl = document.getElementById('file-count');
|
|
|
|
// Ordner + Dateien zählen
|
|
const totalCount = state.folders.length + state.files.length;
|
|
countEl.textContent = `${state.folders.length} Ordner, ${state.files.length} Dateien`;
|
|
|
|
let html = '';
|
|
|
|
// Ordner zuerst
|
|
state.folders.forEach(folder => {
|
|
html += `
|
|
<div class="file-item folder"
|
|
onclick="navigiereZu('${folder.path}')"
|
|
ondragover="event.preventDefault(); this.classList.add('drop-target')"
|
|
ondragleave="this.classList.remove('drop-target')"
|
|
ondrop="handleDrop(event, '${folder.path}')">
|
|
<span class="file-icon">📁</span>
|
|
<span class="file-name">${folder.name}</span>
|
|
<span class="file-size"></span>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
// Dateien
|
|
state.files.forEach(file => {
|
|
const isSelected = state.selectedFile === file.name;
|
|
const icon = getFileIcon(file.name);
|
|
const size = formatSize(file.size);
|
|
|
|
html += `
|
|
<div class="file-item ${isSelected ? 'selected' : ''}"
|
|
onclick="waehltDatei('${file.name}')"
|
|
ondblclick="dateiExternOeffnen()"
|
|
oncontextmenu="zeigeContextMenu(event, '${file.name}')"
|
|
draggable="true"
|
|
ondragstart="handleDragStart(event, '${file.path}')">
|
|
<span class="file-icon">${icon}</span>
|
|
<span class="file-name">${file.name}</span>
|
|
<span class="file-size">${size}</span>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
if (!html) {
|
|
html = '<p class="empty-state">Keine Dateien in diesem Ordner</p>';
|
|
}
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function getFileIcon(name) {
|
|
const ext = name.split('.').pop().toLowerCase();
|
|
const icons = {
|
|
'pdf': '📄',
|
|
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'bmp': '🖼️', 'tiff': '🖼️', 'webp': '🖼️',
|
|
'doc': '📝', 'docx': '📝', 'odt': '📝',
|
|
'xls': '📊', 'xlsx': '📊', 'ods': '📊', 'csv': '📊',
|
|
'zip': '📦', 'rar': '📦', '7z': '📦', 'tar': '📦', 'gz': '📦',
|
|
'txt': '📃', 'md': '📃', 'log': '📃',
|
|
'mp3': '🎵', 'wav': '🎵', 'flac': '🎵',
|
|
'mp4': '🎬', 'avi': '🎬', 'mkv': '🎬',
|
|
'xml': '📋', 'json': '📋', 'html': '📋'
|
|
};
|
|
return icons[ext] || '📎';
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (!bytes) return '';
|
|
const units = ['B', 'KB', 'MB', 'GB'];
|
|
let size = bytes;
|
|
let unit = 0;
|
|
while (size >= 1024 && unit < units.length - 1) {
|
|
size /= 1024;
|
|
unit++;
|
|
}
|
|
return `${size.toFixed(unit > 0 ? 1 : 0)} ${units[unit]}`;
|
|
}
|
|
|
|
function navigiereZu(pfad) {
|
|
ladeOrdnerInhalt(pfad);
|
|
// Ordner im Baum aufklappen
|
|
expandiereBaumPfad(pfad);
|
|
}
|
|
|
|
function expandiereBaumPfad(pfad) {
|
|
// Alle übergeordneten Ordner aufklappen
|
|
const teile = pfad.split('/').filter(t => t);
|
|
let aktuell = '';
|
|
teile.forEach(teil => {
|
|
aktuell += '/' + teil;
|
|
if (!state.expandedFolders.has(aktuell) && aktuell !== pfad) {
|
|
state.expandedFolders.add(aktuell);
|
|
}
|
|
});
|
|
localStorage.setItem('browser_expanded', JSON.stringify([...state.expandedFolders]));
|
|
}
|
|
|
|
function ordnerHoch() {
|
|
const parent = state.currentPath.split('/').slice(0, -1).join('/') || '/';
|
|
navigiereZu(parent);
|
|
}
|
|
|
|
function ordnerAktualisieren() {
|
|
ladeOrdnerInhalt(state.currentPath);
|
|
toast('Aktualisiert', 'info');
|
|
}
|
|
|
|
// ============ Vorschau-Fenster ============
|
|
function oeffneVorschauFenster() {
|
|
if (state.previewWindow && !state.previewWindow.closed) {
|
|
state.previewWindow.focus();
|
|
return;
|
|
}
|
|
|
|
const width = 800;
|
|
const height = 600;
|
|
const left = window.screenX + window.outerWidth - width - 50;
|
|
const top = window.screenY + 50;
|
|
|
|
state.previewWindow = window.open(
|
|
'/browser/preview',
|
|
'dateiverwaltung-preview',
|
|
`width=${width},height=${height},left=${left},top=${top},resizable=yes`
|
|
);
|
|
}
|
|
|
|
// ============ Datei-Auswahl und Vorschau ============
|
|
function waehltDatei(name) {
|
|
state.selectedFile = name;
|
|
state.selectedFilePath = state.currentPath + '/' + name;
|
|
|
|
// UI aktualisieren
|
|
document.querySelectorAll('.file-item').forEach(el => {
|
|
el.classList.remove('selected');
|
|
});
|
|
event.currentTarget.classList.add('selected');
|
|
|
|
// Datei-Info anzeigen (nur wenn Panel sichtbar)
|
|
const file = state.files.find(f => f.name === name);
|
|
if (file && !state.previewPanelHidden) {
|
|
document.getElementById('file-info').classList.remove('hidden');
|
|
document.getElementById('preview-filename').textContent = name;
|
|
document.getElementById('preview-size').textContent = formatSize(file.size);
|
|
document.getElementById('btn-extern').classList.remove('hidden');
|
|
}
|
|
|
|
// Vorschau laden - entweder in separatem Fenster oder inline
|
|
if (state.previewWindowOpen) {
|
|
sendeAnVorschau(state.selectedFilePath, name);
|
|
// Inline-Vorschau zeigt Hinweis (nur wenn Panel sichtbar)
|
|
if (!state.previewPanelHidden) {
|
|
document.getElementById('preview-container').innerHTML = `
|
|
<div class="preview-placeholder">
|
|
<span>Vorschau wird im separaten Fenster angezeigt</span>
|
|
</div>
|
|
`;
|
|
}
|
|
} else if (!state.previewPanelHidden) {
|
|
ladeVorschau(state.selectedFilePath, name);
|
|
}
|
|
}
|
|
|
|
async function ladeVorschau(pfad, name) {
|
|
const container = document.getElementById('preview-container');
|
|
if (!container) return;
|
|
|
|
const ext = name.split('.').pop().toLowerCase();
|
|
|
|
// PDF Vorschau - mit fit-to-page für erste Seite
|
|
if (ext === 'pdf') {
|
|
container.innerHTML = `<iframe class="preview-pdf" src="/api/file/preview?path=${encodeURIComponent(pfad)}&t=${Date.now()}#page=1&view=FitV"></iframe>`;
|
|
return;
|
|
}
|
|
|
|
// Bild Vorschau
|
|
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff'].includes(ext)) {
|
|
container.innerHTML = `<img class="preview-image" src="/api/file/preview?path=${encodeURIComponent(pfad)}&t=${Date.now()}" alt="${name}">`;
|
|
return;
|
|
}
|
|
|
|
// Text-basierte Dateien
|
|
if (['txt', 'md', 'log', 'xml', 'json', 'csv', 'html', 'css', 'js'].includes(ext)) {
|
|
try {
|
|
const result = await api(`/file/text?path=${encodeURIComponent(pfad)}`);
|
|
if (result.content) {
|
|
container.innerHTML = `<pre class="preview-text">${escapeHtml(result.content)}</pre>`;
|
|
} else {
|
|
zeigeKeineVorschau(name, ext);
|
|
}
|
|
} catch (e) {
|
|
zeigeKeineVorschau(name, ext);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Keine Vorschau verfügbar
|
|
zeigeKeineVorschau(name, ext);
|
|
}
|
|
|
|
function zeigeKeineVorschau(name, ext) {
|
|
const icon = getFileIcon(name);
|
|
const container = document.getElementById('preview-container');
|
|
if (container) {
|
|
container.innerHTML = `
|
|
<div class="preview-unavailable">
|
|
<div class="file-type-icon">${icon}</div>
|
|
<p>Keine Vorschau für .${ext} Dateien</p>
|
|
<button class="btn btn-primary" onclick="dateiExternOeffnen()">🔗 Extern öffnen</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function zeigeLeereVorschau() {
|
|
const fileInfo = document.getElementById('file-info');
|
|
const btnExtern = document.getElementById('btn-extern');
|
|
const container = document.getElementById('preview-container');
|
|
|
|
if (fileInfo) fileInfo.classList.add('hidden');
|
|
if (btnExtern) btnExtern.classList.add('hidden');
|
|
if (container) {
|
|
container.innerHTML = `
|
|
<div class="preview-placeholder">
|
|
<span>Datei auswählen um Vorschau zu sehen</span>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// ============ Datei-Operationen ============
|
|
function dateiUmbenennen() {
|
|
if (!state.selectedFile) return;
|
|
|
|
document.getElementById('neuer-name').value = state.selectedFile;
|
|
oeffneModal('umbenennen-modal');
|
|
|
|
setTimeout(() => {
|
|
const input = document.getElementById('neuer-name');
|
|
input.focus();
|
|
const dotIndex = state.selectedFile.lastIndexOf('.');
|
|
if (dotIndex > 0) {
|
|
input.setSelectionRange(0, dotIndex);
|
|
} else {
|
|
input.select();
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
async function umbenennenBestaetigen() {
|
|
const neuerName = document.getElementById('neuer-name').value.trim();
|
|
if (!neuerName) {
|
|
toast('Bitte Namen eingeben', 'warning');
|
|
return;
|
|
}
|
|
|
|
if (neuerName === state.selectedFile) {
|
|
schliesseModal('umbenennen-modal');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await api('/file/rename', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
pfad: state.selectedFilePath,
|
|
neuer_name: neuerName
|
|
})
|
|
});
|
|
|
|
if (result.erfolg) {
|
|
toast('Datei umbenannt', 'success');
|
|
schliesseModal('umbenennen-modal');
|
|
state.selectedFile = neuerName;
|
|
state.selectedFilePath = state.currentPath + '/' + neuerName;
|
|
ladeOrdnerInhalt(state.currentPath);
|
|
} else {
|
|
toast(result.fehler || 'Umbenennen fehlgeschlagen', 'error');
|
|
}
|
|
} catch (e) {
|
|
toast('Fehler: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
function dateiVerschieben() {
|
|
if (!state.selectedFile) return;
|
|
|
|
document.getElementById('verschieben-datei').textContent = state.selectedFile;
|
|
state.verschiebenPath = state.currentPath;
|
|
ladeVerschiebenBrowser(state.verschiebenPath);
|
|
oeffneModal('verschieben-modal');
|
|
}
|
|
|
|
async function ladeVerschiebenBrowser(pfad) {
|
|
try {
|
|
const result = await api(`/browse?path=${encodeURIComponent(pfad)}`);
|
|
|
|
if (result.error) {
|
|
toast(result.error, 'error');
|
|
return;
|
|
}
|
|
|
|
state.verschiebenPath = result.current;
|
|
document.getElementById('verschieben-browser-path').textContent = result.current;
|
|
|
|
let html = '';
|
|
|
|
if (result.parent) {
|
|
html += `<li class="file-browser-item" onclick="ladeVerschiebenBrowser('${result.parent}')">
|
|
<span class="file-icon">📁</span> ..
|
|
</li>`;
|
|
}
|
|
|
|
result.entries.forEach(entry => {
|
|
if (entry.type === 'directory') {
|
|
html += `<li class="file-browser-item" onclick="ladeVerschiebenBrowser('${entry.path}')">
|
|
<span class="file-icon">📁</span> ${entry.name}
|
|
</li>`;
|
|
}
|
|
});
|
|
|
|
if (!html) {
|
|
html = '<li class="empty-state">Keine Unterordner</li>';
|
|
}
|
|
|
|
document.getElementById('verschieben-browser-list').innerHTML = html;
|
|
|
|
} catch (e) {
|
|
toast('Fehler beim Laden', 'error');
|
|
}
|
|
}
|
|
|
|
async function verschiebenBestaetigen() {
|
|
if (state.verschiebenPath === state.currentPath) {
|
|
toast('Zielordner ist der aktuelle Ordner', 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await api('/file/move', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
pfad: state.selectedFilePath,
|
|
ziel_ordner: state.verschiebenPath
|
|
})
|
|
});
|
|
|
|
if (result.erfolg) {
|
|
toast('Datei verschoben', 'success');
|
|
schliesseModal('verschieben-modal');
|
|
state.selectedFile = null;
|
|
state.selectedFilePath = null;
|
|
zeigeLeereVorschau();
|
|
ladeOrdnerInhalt(state.currentPath);
|
|
} else {
|
|
toast(result.fehler || 'Verschieben fehlgeschlagen', 'error');
|
|
}
|
|
} catch (e) {
|
|
toast('Fehler: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
function dateiLoeschen() {
|
|
if (!state.selectedFile) return;
|
|
|
|
document.getElementById('loeschen-datei').textContent = state.selectedFile;
|
|
oeffneModal('loeschen-modal');
|
|
}
|
|
|
|
async function loeschenBestaetigen() {
|
|
try {
|
|
const result = await api('/file/delete', {
|
|
method: 'DELETE',
|
|
body: JSON.stringify({
|
|
pfad: state.selectedFilePath
|
|
})
|
|
});
|
|
|
|
if (result.erfolg) {
|
|
toast('Datei gelöscht', 'success');
|
|
schliesseModal('loeschen-modal');
|
|
state.selectedFile = null;
|
|
state.selectedFilePath = null;
|
|
zeigeLeereVorschau();
|
|
ladeOrdnerInhalt(state.currentPath);
|
|
} else {
|
|
toast(result.fehler || 'Löschen fehlgeschlagen', 'error');
|
|
}
|
|
} catch (e) {
|
|
toast('Fehler: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
function dateiExternOeffnen() {
|
|
if (!state.selectedFilePath) return;
|
|
window.open(`/api/file/download?path=${encodeURIComponent(state.selectedFilePath)}`, '_blank');
|
|
}
|
|
|
|
// ============ Drag & Drop ============
|
|
function handleDragStart(event, pfad) {
|
|
event.dataTransfer.setData('text/plain', pfad);
|
|
event.dataTransfer.effectAllowed = 'move';
|
|
}
|
|
|
|
async function handleDrop(event, zielOrdner) {
|
|
event.preventDefault();
|
|
event.currentTarget.classList.remove('drop-target');
|
|
|
|
const quellPfad = event.dataTransfer.getData('text/plain');
|
|
if (!quellPfad || quellPfad === zielOrdner) return;
|
|
|
|
try {
|
|
const result = await api('/file/move', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
pfad: quellPfad,
|
|
ziel_ordner: zielOrdner
|
|
})
|
|
});
|
|
|
|
if (result.erfolg) {
|
|
toast('Datei verschoben', 'success');
|
|
ladeOrdnerInhalt(state.currentPath);
|
|
} else {
|
|
toast(result.fehler || 'Verschieben fehlgeschlagen', 'error');
|
|
}
|
|
} catch (e) {
|
|
toast('Fehler: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
// ============ Context Menu ============
|
|
function zeigeContextMenu(event, dateiname) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const vorher = document.querySelector('.context-menu');
|
|
if (vorher) vorher.remove();
|
|
|
|
state.selectedFile = dateiname;
|
|
state.selectedFilePath = state.currentPath + '/' + dateiname;
|
|
|
|
const menu = document.createElement('div');
|
|
menu.className = 'context-menu';
|
|
menu.innerHTML = `
|
|
<div class="context-menu-item" onclick="dateiExternOeffnen()">🔗 Öffnen</div>
|
|
<div class="context-menu-separator"></div>
|
|
<div class="context-menu-item" onclick="dateiUmbenennen()">✏️ Umbenennen</div>
|
|
<div class="context-menu-item" onclick="dateiVerschieben()">📦 Verschieben</div>
|
|
<div class="context-menu-separator"></div>
|
|
<div class="context-menu-item danger" onclick="dateiLoeschen()">🗑 Löschen</div>
|
|
`;
|
|
|
|
menu.style.left = event.clientX + 'px';
|
|
menu.style.top = event.clientY + 'px';
|
|
|
|
document.body.appendChild(menu);
|
|
|
|
const rect = menu.getBoundingClientRect();
|
|
if (rect.right > window.innerWidth) {
|
|
menu.style.left = (window.innerWidth - rect.width - 10) + 'px';
|
|
}
|
|
if (rect.bottom > window.innerHeight) {
|
|
menu.style.top = (window.innerHeight - rect.height - 10) + 'px';
|
|
}
|
|
}
|
|
|
|
// ============ Keyboard Shortcuts ============
|
|
function handleKeydown(event) {
|
|
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') return;
|
|
|
|
switch (event.key) {
|
|
case 'F2':
|
|
event.preventDefault();
|
|
if (state.selectedFile) dateiUmbenennen();
|
|
break;
|
|
case 'Delete':
|
|
event.preventDefault();
|
|
if (state.selectedFile) dateiLoeschen();
|
|
break;
|
|
case 'F5':
|
|
event.preventDefault();
|
|
ordnerAktualisieren();
|
|
break;
|
|
case 'Backspace':
|
|
event.preventDefault();
|
|
ordnerHoch();
|
|
break;
|
|
case 'Enter':
|
|
if (state.selectedFile) {
|
|
dateiExternOeffnen();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ============ Resize Handles ============
|
|
function initResizeHandles() {
|
|
const main = document.getElementById('browser-main');
|
|
const treePane = document.getElementById('pane-tree');
|
|
const listPane = document.getElementById('pane-list');
|
|
const handle1 = document.getElementById('resize-handle-1');
|
|
const handle2 = document.getElementById('resize-handle-2');
|
|
|
|
if (!main || !treePane || !listPane) return;
|
|
|
|
const isVertical = main.classList.contains('vertical') || window.innerWidth <= 1000;
|
|
|
|
// Handle 1: Zwischen Baum und Liste
|
|
if (handle1) {
|
|
let isResizing = false;
|
|
|
|
handle1.onmousedown = (e) => {
|
|
isResizing = true;
|
|
handle1.classList.add('active');
|
|
document.body.style.cursor = isVertical ? 'row-resize' : 'col-resize';
|
|
document.body.style.userSelect = 'none';
|
|
e.preventDefault();
|
|
};
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isResizing) return;
|
|
|
|
if (isVertical) {
|
|
// Vertikaler Modus: Höhe ändern
|
|
const containerTop = main.getBoundingClientRect().top;
|
|
const newHeight = Math.max(80, Math.min(e.clientY - containerTop, 300));
|
|
treePane.style.height = newHeight + 'px';
|
|
} else {
|
|
// Horizontaler Modus: Breite ändern
|
|
const containerLeft = main.getBoundingClientRect().left;
|
|
const newWidth = Math.max(150, Math.min(e.clientX - containerLeft, 400));
|
|
treePane.style.width = newWidth + 'px';
|
|
}
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (isResizing) {
|
|
isResizing = false;
|
|
handle1.classList.remove('active');
|
|
document.body.style.cursor = '';
|
|
document.body.style.userSelect = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handle 2: Zwischen Liste und Vorschau
|
|
if (handle2) {
|
|
let isResizing = false;
|
|
|
|
handle2.onmousedown = (e) => {
|
|
isResizing = true;
|
|
handle2.classList.add('active');
|
|
document.body.style.cursor = isVertical ? 'row-resize' : 'col-resize';
|
|
document.body.style.userSelect = 'none';
|
|
e.preventDefault();
|
|
};
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isResizing) return;
|
|
|
|
if (isVertical) {
|
|
// Vertikaler Modus: Höhe ändern
|
|
const treePaneHeight = treePane.offsetHeight;
|
|
const handle1Height = handle1 ? handle1.offsetHeight : 0;
|
|
const containerTop = main.getBoundingClientRect().top;
|
|
const offset = treePaneHeight + handle1Height;
|
|
const newHeight = Math.max(100, Math.min(e.clientY - containerTop - offset, 400));
|
|
listPane.style.height = newHeight + 'px';
|
|
} else {
|
|
// Horizontaler Modus: Breite ändern
|
|
const treePaneWidth = treePane.offsetWidth;
|
|
const handle1Width = handle1 ? handle1.offsetWidth : 0;
|
|
const containerLeft = main.getBoundingClientRect().left;
|
|
const offset = treePaneWidth + handle1Width;
|
|
const newWidth = Math.max(200, Math.min(e.clientX - containerLeft - offset, 600));
|
|
listPane.style.width = newWidth + 'px';
|
|
}
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (isResizing) {
|
|
isResizing = false;
|
|
handle2.classList.remove('active');
|
|
document.body.style.cursor = '';
|
|
document.body.style.userSelect = '';
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ============ Modal Helper ============
|
|
function oeffneModal(id) {
|
|
document.getElementById(id).classList.remove('hidden');
|
|
}
|
|
|
|
function schliesseModal(id) {
|
|
document.getElementById(id).classList.add('hidden');
|
|
}
|
|
|
|
// ============ Toast Notifications ============
|
|
function toast(message, type = 'info') {
|
|
const container = document.getElementById('toast-container');
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast ${type}`;
|
|
toast.textContent = message;
|
|
container.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.style.animation = 'fadeOut 0.3s ease';
|
|
setTimeout(() => toast.remove(), 300);
|
|
}, 3000);
|
|
}
|