/** * 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 || '

Keine Ordner

'; } function renderBaumKnoten(pfad, tiefe) { const knoten = state.folderTree[pfad]; if (!knoten) { // Knoten existiert nicht im Cache - erstelle Platzhalter return `
📁 ${pfad.split('/').pop() || pfad}
`; } 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 = `
${hatKinder || !istGeladen ? (istExpanded ? '▼' : '▶') : ''} ${istExpanded ? '📂' : '📁'} ${knoten.name}
`; // 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 = `/`; let pfad = ''; teile.forEach((teil, index) => { pfad += '/' + teil; const istLetzter = index === teile.length - 1; if (istLetzter) { html += `/`; html += `${teil}`; } else { html += `/`; html += `${teil}`; } }); 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 += `
📁 ${folder.name}
`; }); // Dateien state.files.forEach(file => { const isSelected = state.selectedFile === file.name; const icon = getFileIcon(file.name); const size = formatSize(file.size); html += `
${icon} ${file.name} ${size}
`; }); if (!html) { html = '

Keine Dateien in diesem Ordner

'; } 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 = `
Vorschau wird im separaten Fenster angezeigt
`; } } 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 = ``; return; } // Bild Vorschau if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff'].includes(ext)) { container.innerHTML = `${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 = `
${escapeHtml(result.content)}
`; } 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 = `
${icon}

Keine Vorschau für .${ext} Dateien

`; } } 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 = `
Datei auswählen um Vorschau zu sehen
`; } } 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 += `
  • 📁 ..
  • `; } result.entries.forEach(entry => { if (entry.type === 'directory') { html += `
  • 📁 ${entry.name}
  • `; } }); if (!html) { html = '
  • Keine Unterordner
  • '; } 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 = `
    🔗 Öffnen
    ✏️ Umbenennen
    📦 Verschieben
    🗑 Löschen
    `; 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); }