// renderer.js — Grid-UI + Navigation + Drag'n'Drop mit Fehlerbehandlung const $ = id => document.getElementById(id); let selectedFolder = null; let giteaCache = {}; // Navigations-Status für die Explorer-Ansicht let currentState = { view: 'none', // 'local', 'gitea-list', 'gitea-repo' owner: null, repo: null, path: '' }; function setStatus(txt) { const s = $('status'); if (s) s.innerText = txt || ''; } /* ------------------------- PROGRESS UI ------------------------- */ function ensureProgressUI() { if ($('folderProgressContainer')) return; const container = document.createElement('div'); container.id = 'folderProgressContainer'; container.style.cssText = ` position: fixed; left: 50%; top: 12px; transform: translateX(-50%); z-index: 10000; width: 480px; max-width: 90%; padding: 12px 16px; background: rgba(20,20,30,0.98); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.6); color: #fff; font-family: sans-serif; display: none; backdrop-filter: blur(10px); border: 1px solid rgba(0, 212, 255, 0.2); `; const text = document.createElement('div'); text.id = 'folderProgressText'; text.style.cssText = ` margin-bottom: 8px; font-size: 13px; font-weight: 600; color: #00d4ff; `; container.appendChild(text); const barWrap = document.createElement('div'); barWrap.style.cssText = ` width: 100%; height: 12px; background: rgba(255,255,255,0.1); border-radius: 6px; overflow: hidden; position: relative; `; const bar = document.createElement('div'); bar.id = 'folderProgressBar'; bar.style.cssText = ` width: 0%; height: 100%; background: linear-gradient(90deg, #00d4ff, #8b5cf6); transition: width 200ms ease-out; border-radius: 6px; `; barWrap.appendChild(bar); container.appendChild(barWrap); document.body.appendChild(container); } function showProgress(percent, text) { ensureProgressUI(); const container = $('folderProgressContainer'); const bar = $('folderProgressBar'); const txt = $('folderProgressText'); if (txt) txt.innerText = text || ''; if (bar) bar.style.width = `${Math.min(100, Math.max(0, percent))}%`; if (container) container.style.display = 'block'; } function hideProgress() { const container = $('folderProgressContainer'); if (container) { setTimeout(() => { container.style.display = 'none'; }, 500); } } /* ------------------------- ADVANCED FILE EDITOR - WITH TABS, UNDO/REDO, AUTO-SAVE, LINE NUMBERS ------------------------- */ // Editor State let openTabs = {}; // { filePath: { name, content, originalContent, dirty, icon, history, historyIndex } } let currentActiveTab = null; let autoSaveTimer = null; let autoSaveInterval = 3000; // 3 sekunden // Initialize editor function initEditor() { const textarea = $('fileEditorContent'); if (!textarea) return; textarea.addEventListener('input', () => { updateCurrentTab(); updateLineNumbers(); updateEditorStats(); triggerAutoSave(); }); textarea.addEventListener('scroll', () => { const lineNumbers = $('lineNumbers'); if (lineNumbers) lineNumbers.scrollTop = textarea.scrollTop; }); textarea.addEventListener('keydown', (e) => { // Ctrl+Z - Undo if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) { e.preventDefault(); undoChange(); } // Ctrl+Shift+Z or Ctrl+Y - Redo if (((e.ctrlKey || e.metaKey) && e.key === 'z' && e.shiftKey) || ((e.ctrlKey || e.metaKey) && e.key === 'y')) { e.preventDefault(); redoChange(); } // Tab insertion if (e.key === 'Tab') { e.preventDefault(); const start = textarea.selectionStart; const end = textarea.selectionEnd; textarea.value = textarea.value.substring(0, start) + '\t' + textarea.value.substring(end); textarea.selectionStart = textarea.selectionEnd = start + 1; updateCurrentTab(); updateLineNumbers(); } }); } // Add new tab function addTab(filePath, fileName, content, isGitea = false, owner = null, repo = null) { openTabs[filePath] = { name: fileName, content: content, originalContent: content, dirty: false, icon: getFileIcon(fileName), isGitea, owner, repo, history: [content], historyIndex: 0 }; currentActiveTab = filePath; renderTabs(); updateEditor(); // Kann async sein } // Remove tab function removeTab(filePath) { delete openTabs[filePath]; if (currentActiveTab === filePath) { const paths = Object.keys(openTabs); currentActiveTab = paths.length > 0 ? paths[0] : null; } if (Object.keys(openTabs).length === 0) { closeFileEditor(); } else { renderTabs(); updateEditor(); } } // Switch tab function switchTab(filePath) { if (openTabs[filePath]) { currentActiveTab = filePath; renderTabs(); updateEditor(); // Kann async sein, aber wir warten nicht } } // Render tabs function renderTabs() { const tabsContainer = $('fileEditorTabs'); if (!tabsContainer) return; tabsContainer.innerHTML = ''; Object.entries(openTabs).forEach(([filePath, tab]) => { const tabEl = document.createElement('div'); tabEl.className = `editor-tab ${currentActiveTab === filePath ? 'active' : ''}`; const nameEl = document.createElement('div'); nameEl.className = 'editor-tab-name'; const iconEl = document.createElement('span'); iconEl.textContent = tab.icon; const nameSpan = document.createElement('span'); nameSpan.textContent = tab.name; if (tab.dirty) { const dirtyEl = document.createElement('span'); dirtyEl.className = 'editor-tab-dirty'; dirtyEl.textContent = '●'; nameEl.appendChild(iconEl); nameEl.appendChild(nameSpan); nameEl.appendChild(dirtyEl); } else { nameEl.appendChild(iconEl); nameEl.appendChild(nameSpan); } const closeBtn = document.createElement('button'); closeBtn.className = 'editor-tab-close'; closeBtn.textContent = '✕'; closeBtn.addEventListener('click', (e) => { e.stopPropagation(); if (tab.dirty && !confirm(`${tab.name} hat ungespeicherte Änderungen. Wirklich schließen?`)) { return; } removeTab(filePath); }); tabEl.appendChild(nameEl); tabEl.appendChild(closeBtn); tabEl.addEventListener('click', () => switchTab(filePath)); tabsContainer.appendChild(tabEl); }); } // Update current tab content function updateCurrentTab() { if (!currentActiveTab) return; const textarea = $('fileEditorContent'); const content = textarea.value; const tab = openTabs[currentActiveTab]; if (!tab) return; tab.content = content; tab.dirty = (content !== tab.originalContent); renderTabs(); } // Update editor display async function updateEditor() { if (!currentActiveTab || !openTabs[currentActiveTab]) return; const tab = openTabs[currentActiveTab]; const textarea = $('fileEditorContent'); const imagePreview = $('imagePreview'); // Prüfe ob es eine Bilddatei ist const isImage = /\.(png|jpg|jpeg|gif|webp|svg)$/i.test(currentActiveTab); if (isImage) { // Zeige Bild statt Textarea if (textarea) textarea.classList.add('hidden'); if (imagePreview) { imagePreview.classList.remove('hidden'); let imgSrc = ''; if (tab.isGitea) { // Gitea-Bild: Lade via API try { const filePath = currentActiveTab.replace(`gitea://${tab.owner}/${tab.repo}/`, ''); const response = await window.electronAPI.readGiteaFile({ owner: tab.owner, repo: tab.repo, path: filePath, ref: 'main' }); if (response.ok) { // Content ist Base64 Text, konvertiere zu Data URL const imageData = response.content; const ext = filePath.split('.').pop().toLowerCase(); const mimeType = { 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'gif': 'image/gif', 'webp': 'image/webp', 'svg': 'image/svg+xml' }[ext] || 'image/png'; imgSrc = `data:${mimeType};base64,${imageData}`; } } catch (error) { console.error('Error loading Gitea image:', error); imagePreview.innerHTML = '
Fehler beim Laden des Bildes
'; return; } } else { // Lokale Datei imgSrc = 'file:///' + currentActiveTab.replace(/\\/g, '/'); } // Erstelle Bild-Element mit verbesserter Darstellung const img = document.createElement('img'); img.src = imgSrc; img.alt = tab.name; img.style.cssText = ` max-width: 100%; max-height: 85vh; width: auto; height: auto; display: block; margin: 0 auto; object-fit: contain; cursor: zoom-in; `; // Click zum Zoomen (Original-Größe) let isZoomed = false; img.onclick = function() { if (isZoomed) { img.style.maxWidth = '100%'; img.style.maxHeight = '85vh'; img.style.cursor = 'zoom-in'; isZoomed = false; } else { img.style.maxWidth = 'none'; img.style.maxHeight = 'none'; img.style.cursor = 'zoom-out'; isZoomed = true; } }; img.onerror = function() { imagePreview.innerHTML = '
Bild konnte nicht geladen werden
'; }; // Container für zentrierte Anzeige imagePreview.innerHTML = ''; imagePreview.style.cssText = ` display: flex; justify-content: center; align-items: center; min-height: 400px; overflow: auto; padding: 20px; `; imagePreview.appendChild(img); } } else { // Zeige Text Editor if (textarea) { textarea.classList.remove('hidden'); textarea.value = tab.content; updateLineNumbers(); updateEditorStats(); } if (imagePreview) imagePreview.classList.add('hidden'); } updateTabInfo(); } // Update tab info header function updateTabInfo() { const tab = openTabs[currentActiveTab]; if (!tab) return; $('fileEditorName').textContent = tab.name; $('fileEditorIcon').textContent = tab.icon; const pathText = tab.isGitea ? `Gitea: ${tab.owner}/${tab.repo}/${currentActiveTab}` : `Pfad: ${currentActiveTab}`; $('fileEditorPath').textContent = pathText; const lines = tab.content.split('\n').length; const bytes = new Blob([tab.content]).size; $('fileEditorStats').textContent = `${lines} Zeilen • ${bytes} Bytes`; } // Update line numbers function updateLineNumbers() { const textarea = $('fileEditorContent'); const lineNumbers = $('lineNumbers'); if (!textarea || !lineNumbers) return; const lines = textarea.value.split('\n').length; let html = ''; for (let i = 1; i <= lines; i++) { html += i + '\n'; } lineNumbers.textContent = html; lineNumbers.scrollTop = textarea.scrollTop; } // Update editor stats (cursor position) function updateEditorStats() { const textarea = $('fileEditorContent'); if (!textarea) return; const lines = textarea.value.split('\n').length; const startPos = textarea.selectionStart; const textBeforeCursor = textarea.value.substring(0, startPos); const line = textBeforeCursor.split('\n').length; const col = startPos - textBeforeCursor.lastIndexOf('\n'); $('fileEditorCursor').textContent = `Zeile ${line}, Spalte ${col}`; } // Undo function undoChange() { if (!currentActiveTab) return; const tab = openTabs[currentActiveTab]; if (tab.historyIndex > 0) { tab.historyIndex--; const textarea = $('fileEditorContent'); textarea.value = tab.history[tab.historyIndex]; updateCurrentTab(); updateLineNumbers(); updateEditorStats(); } } // Redo function redoChange() { if (!currentActiveTab) return; const tab = openTabs[currentActiveTab]; if (tab.historyIndex < tab.history.length - 1) { tab.historyIndex++; const textarea = $('fileEditorContent'); textarea.value = tab.history[tab.historyIndex]; updateCurrentTab(); updateLineNumbers(); updateEditorStats(); } } // Push to history function pushToHistory(content) { if (!currentActiveTab) return; const tab = openTabs[currentActiveTab]; // Remove any redo history tab.history = tab.history.slice(0, tab.historyIndex + 1); tab.history.push(content); tab.historyIndex++; // Limit history to 50 items if (tab.history.length > 50) { tab.history.shift(); tab.historyIndex--; } } // Auto-Save function triggerAutoSave() { clearTimeout(autoSaveTimer); autoSaveTimer = setTimeout(() => { saveCurrentFile(true); }, autoSaveInterval); } function showAutoSaveIndicator() { const indicator = $('autoSaveStatus'); if (indicator) { indicator.style.display = 'inline'; setTimeout(() => { indicator.style.display = 'none'; }, 2000); } } function closeFileEditor() { // Überprüfe auf ungespeicherte Änderungen const unsaved = Object.entries(openTabs).filter(([_, tab]) => tab.dirty); if (unsaved.length > 0) { if (!confirm(`${unsaved.length} Datei(en) haben ungespeicherte Änderungen. Wirklich schließen?`)) { return; } } openTabs = {}; currentActiveTab = null; clearTimeout(autoSaveTimer); const modal = $('fileEditorModal'); if (modal) modal.classList.add('hidden'); } async function openFileEditor(filePath, fileName) { try { console.log('🔍 Opening file:', filePath); // Wenn bereits offen, nur switchen if (openTabs[filePath]) { switchTab(filePath); const modal = $('fileEditorModal'); if (modal) modal.classList.remove('hidden'); return; } // Prüfe ob es eine Bilddatei ist const isImage = /\.(png|jpg|jpeg|gif|webp|svg)$/i.test(fileName); if (isImage) { // Für Bilder brauchen wir keinen Content zu lesen addTab(filePath, fileName, '', false, null, null); } else { // Lese Text-Datei const response = await window.electronAPI.readFile({ path: filePath }); if (response.ok) { addTab(filePath, fileName, response.content); } else { alert(`Fehler: ${response.error}`); return; } } const modal = $('fileEditorModal'); if (modal) { modal.classList.remove('hidden'); initEditor(); $('fileEditorContent').focus(); } setStatus(`Editiere: ${fileName}`); console.log('✅ File opened'); } catch (error) { console.error('Error opening file:', error); alert('Fehler beim Öffnen der Datei'); } } async function openGiteaFileInEditor(owner, repo, filePath, fileName) { try { console.log('🔍 Loading Gitea file:', owner, repo, filePath); setStatus('Lädt Datei...'); // Wenn bereits offen, nur switchen const vPath = `gitea://${owner}/${repo}/${filePath}`; if (openTabs[vPath]) { switchTab(vPath); const modal = $('fileEditorModal'); if (modal) modal.classList.remove('hidden'); return; } // Lade Datei-Content vom Gitea Handler const response = await window.electronAPI.readGiteaFile({ owner, repo, path: filePath, ref: 'main' }); if (response.ok) { addTab(vPath, fileName, response.content, true, owner, repo); const modal = $('fileEditorModal'); if (modal) { modal.classList.remove('hidden'); initEditor(); $('fileEditorContent').focus(); } setStatus(`Editiere: ${fileName}`); console.log('✅ Gitea file opened'); } else { alert(`Fehler: ${response.error}`); setStatus('Fehler beim Laden der Datei'); } } catch (error) { console.error('Error opening Gitea file:', error); alert('Fehler beim Öffnen der Datei'); setStatus('Fehler'); } } function getFileIcon(fileName) { const ext = fileName.split('.').pop()?.toLowerCase(); const icons = { 'js': '🟨', 'jsx': '⚛️', 'ts': '🔵', 'tsx': '⚛️', 'py': '🐍', 'java': '☕', 'cpp': '⚙️', 'c': '⚙️', 'html': '🌐', 'css': '🎨', 'scss': '🎨', 'json': '📋', 'md': '📝', 'txt': '📄', 'xml': '📦', 'yaml': '⚙️', 'yml': '⚙️', 'env': '🔑', 'sh': '💻', 'bat': '💻' }; return icons[ext] || '📄'; } /* ------------------------- SEARCH & REPLACE ------------------------- */ function toggleSearch() { const searchBar = $('searchBar'); if (searchBar.classList.contains('hidden')) { searchBar.classList.remove('hidden'); $('searchInput').focus(); } else { searchBar.classList.add('hidden'); } } function performSearch() { const searchTerm = $('searchInput').value; const textarea = $('fileEditorContent'); if (!searchTerm || !textarea) return; const text = textarea.value; const regex = new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); const matches = [...text.matchAll(regex)]; $('searchInfo').textContent = matches.length > 0 ? `${matches.length} gefunden` : '0 gefunden'; if (matches.length > 0) { const firstMatch = matches[0]; textarea.setSelectionRange(firstMatch.index, firstMatch.index + firstMatch[0].length); textarea.focus(); } } function replaceOnce() { const searchTerm = $('searchInput').value; const replaceTerm = $('replaceInput').value; const textarea = $('fileEditorContent'); if (!searchTerm || !textarea) return; const text = textarea.value; const newText = text.replace(new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'), replaceTerm); textarea.value = newText; pushToHistory(newText); updateCurrentTab(); updateLineNumbers(); performSearch(); } function replaceAll() { const searchTerm = $('searchInput').value; const replaceTerm = $('replaceInput').value; const textarea = $('fileEditorContent'); if (!searchTerm || !textarea) return; const text = textarea.value; const newText = text.replace(new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), replaceTerm); textarea.value = newText; pushToHistory(newText); updateCurrentTab(); updateLineNumbers(); performSearch(); } async function saveCurrentFile(isAutoSave = false) { if (!currentActiveTab) return; const tab = openTabs[currentActiveTab]; // Prüfe ob es eine Bilddatei ist if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(currentActiveTab)) { alert('Bilder können nicht bearbeitet werden'); return; } const textarea = $('fileEditorContent'); const content = textarea.value; if (!isAutoSave) setStatus('Speichert...'); try { let response; // Prüfe ob es eine Gitea-Datei ist if (tab.isGitea) { response = await window.electronAPI.writeGiteaFile({ owner: tab.owner, repo: tab.repo, path: currentActiveTab.replace(`gitea://${tab.owner}/${tab.repo}/`, ''), content: content, ref: 'main' }); } else { // Lokale Datei response = await window.electronAPI.writeFile({ path: currentActiveTab, content: content }); } if (response.ok) { tab.originalContent = content; tab.dirty = false; // Push current state to history pushToHistory(content); renderTabs(); if (isAutoSave) { showAutoSaveIndicator(); } else { setStatus(`✓ Gespeichert: ${tab.name}`); } console.log('✅ File saved'); } else { alert(`Fehler: ${response.error}`); } } catch (error) { console.error('Error saving file:', error); alert('Fehler beim Speichern'); } } function updateEditorStats() { const textarea = $('fileEditorContent'); if (!textarea) return; const lines = textarea.value.split('\n').length; const startPos = textarea.selectionStart; const textBeforeCursor = textarea.value.substring(0, startPos); const line = textBeforeCursor.split('\n').length; const col = startPos - textBeforeCursor.lastIndexOf('\n'); $('fileEditorCursor').textContent = `Zeile ${line}, Spalte ${col}`; } /* ------------------------- MARKDOWN PARSER ------------------------- */ function parseMarkdownToHTML(markdown) { if (!markdown) return ''; let html = markdown; // Check if content already contains HTML (starts with < or has closing tags) const hasHTML = /<[a-zA-Z][\s\S]*>/.test(html); if (!hasHTML) { // Only escape and parse if no HTML present html = html.replace(/&/g, '&') .replace(//g, '>'); } // Convert markdown patterns // Headings: ### Title →

Title

html = html.replace(/^###### (.*?)$/gm, '
$1
'); html = html.replace(/^##### (.*?)$/gm, '
$1
'); html = html.replace(/^#### (.*?)$/gm, '

$1

'); html = html.replace(/^### (.*?)$/gm, '

$1

'); html = html.replace(/^## (.*?)$/gm, '

$1

'); html = html.replace(/^# (.*?)$/gm, '

$1

'); // Horizontal rule: --- or *** or ___ html = html.replace(/^\-{3,}$/gm, '
'); html = html.replace(/^\*{3,}$/gm, '
'); html = html.replace(/^_{3,}$/gm, '
'); // Bold: **text** or __text__ html = html.replace(/\*\*(.*?)\*\*/g, '$1'); html = html.replace(/__(.*?)__/g, '$1'); // Italic: *text* or _text_ (but not within words) html = html.replace(/\s\*(.*?)\*\s/g, ' $1 '); html = html.replace(/\s_(.*?)_\s/g, ' $1 '); // Convert line breaks to
html = html.replace(/\n/g, '
'); // Wrap plain text in paragraphs (text not already in tags) let lines = html.split('
'); lines = lines.map(line => { line = line.trim(); if (line && !line.match(/^ 0) { // Only wrap if not already a tag if (!line.match(/^<(h[1-6]|hr|p|div|ul|ol|li|em|strong|b|i)/)) { return '

' + line + '

'; } } return line; }); html = lines.join('
'); return html; } /* ------------------------- NAVIGATION & UI UPDATES ------------------------- */ function updateNavigationUI() { const btnBack = $('btnBack'); if (!btnBack) return; // Back Button zeigen, wenn wir in einem Repo oder tief in Ordnern sind if (currentState.view === 'gitea-repo' || (currentState.view === 'gitea-list' && currentState.path !== '')) { btnBack.classList.remove('hidden'); } else { btnBack.classList.add('hidden'); } } /* ------------------------- GITEA CORE LOGIK (GRID) ------------------------- */ async function loadGiteaRepos() { currentState.view = 'gitea-list'; currentState.path = ''; updateNavigationUI(); // Verstecke Commits & Releases-Buttons in Repo-Liste const btnCommits = $('btnCommits'); const btnReleases = $('btnReleases'); if (btnCommits) btnCommits.classList.add('hidden'); if (btnReleases) btnReleases.classList.add('hidden'); // WICHTIG: Grid-Layout zurücksetzen const grid = $('explorerGrid'); if (grid) { grid.style.gridTemplateColumns = ''; } setStatus('Loading Gitea repos...'); try { const res = await window.electronAPI.listGiteaRepos(); if (!res.ok) { setStatus('Failed to load repos: ' + (res.error || 'Unknown error')); return; } const grid = $('explorerGrid'); if (!grid) return; grid.innerHTML = ''; if (!res.repos || res.repos.length === 0) { grid.innerHTML = '
Keine Repositories gefunden
'; setStatus('No repositories found'); return; } // --- NEU: Suchfeld für Projekte --- const searchContainer = document.createElement('div'); searchContainer.style.cssText = 'grid-column: 1/-1; margin-bottom: 20px;'; const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = '🔍 Projekt suchen...'; searchInput.style.cssText = ` width: 100%; padding: 12px 16px; border-radius: var(--radius-md); border: 1px solid rgba(255, 255, 255, 0.1); background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px; outline: none; box-sizing: border-box; `; // Search Focus Effekt searchInput.addEventListener('focus', () => { searchInput.style.borderColor = 'var(--accent-primary)'; }); searchInput.addEventListener('blur', () => { searchInput.style.borderColor = 'rgba(255, 255, 255, 0.1)'; }); searchContainer.appendChild(searchInput); grid.appendChild(searchContainer); // Search Logic searchInput.addEventListener('input', (e) => { const val = e.target.value.toLowerCase(); const cards = grid.querySelectorAll('.item-card'); cards.forEach(card => { const name = card.querySelector('.item-name').textContent.toLowerCase(); if (name.includes(val)) { card.style.display = 'flex'; } else { card.style.display = 'none'; } }); }); // ----------------------------------- res.repos.forEach(repo => { let owner = (repo.owner && (repo.owner.login || repo.owner.username)) || null; let repoName = repo.name; let cloneUrl = repo.clone_url || repo.clone_url_ssh; const card = document.createElement('div'); card.className = 'item-card'; card.innerHTML = `
📦
${repoName}
`; card.dataset.cloneUrl = cloneUrl; // --- Nativer Drag Start (Download) --- card.draggable = true; card.addEventListener('dragstart', async (ev) => { ev.preventDefault(); setStatus(`Preparing download for ${repoName}...`); showProgress(0, `Preparing ${repoName}...`); try { const resDrag = await window.electronAPI.prepareDownloadDrag({ owner, repo: repoName, path: '' }); if (resDrag.ok) { window.electronAPI.startNativeDrag(resDrag.tempPath); setStatus('Ready to drag'); } else { setStatus('Download preparation failed'); } } catch (error) { console.error('Drag preparation error:', error); setStatus('Error preparing download'); } finally { hideProgress(); } }); // --- Nativer Drop (Upload in Repo Root) --- card.addEventListener('dragover', (ev) => { ev.preventDefault(); ev.stopPropagation(); card.classList.add('drag-target'); }); card.addEventListener('dragleave', (ev) => { ev.preventDefault(); ev.stopPropagation(); card.classList.remove('drag-target'); }); card.addEventListener('drop', async (ev) => { ev.preventDefault(); ev.stopPropagation(); card.classList.remove('drag-target'); const files = ev.dataTransfer.files; if (!files || files.length === 0) { setStatus("Keine Dateien zum Upload gefunden."); return; } const paths = Array.from(files).map(f => f.path); setStatus(`Starte Upload von ${paths.length} Elementen...`); for (const p of paths) { const baseName = p.split(/[\\/]/).pop(); showProgress(0, `Sende: ${baseName}`); try { const res = await window.electronAPI.uploadAndPush({ localFolder: p, owner, repo: repoName, destPath: '', cloneUrl, branch: 'main' }); if (!res.ok) { console.error("Upload Fehler:", res.error); setStatus("Fehler: " + res.error); } } catch (err) { console.error("Kritischer Upload Fehler:", err); setStatus("Upload fehlgeschlagen"); } } hideProgress(); setStatus('Upload abgeschlossen'); }); card.onclick = () => loadRepoContents(owner, repoName, ''); card.oncontextmenu = (ev) => showRepoContextMenu(ev, owner, repoName, cloneUrl, card); grid.appendChild(card); }); setStatus(`Loaded ${res.repos.length} repos`); } catch (error) { console.error('Error loading repos:', error); setStatus('Error loading repositories'); } } async function loadRepoContents(owner, repo, path) { currentState.view = 'gitea-repo'; currentState.owner = owner; currentState.repo = repo; currentState.path = path; updateNavigationUI(); // Zeige Commits & Releases-Buttons wenn wir in einem Repo sind const btnCommits = $('btnCommits'); const btnReleases = $('btnReleases'); if (btnCommits) { btnCommits.classList.remove('hidden'); btnCommits.onclick = () => loadCommitHistory(owner, repo, 'main'); } if (btnReleases) { btnReleases.classList.remove('hidden'); btnReleases.onclick = () => loadRepoReleases(owner, repo); } // WICHTIG: Grid-Layout zurücksetzen const grid = $('explorerGrid'); if (grid) { grid.style.gridTemplateColumns = ''; } setStatus(`Loading: /${path || 'root'}`); try { const res = await window.electronAPI.getGiteaRepoContents({ owner, repo, path, ref: 'main' }); if (!res.ok) { setStatus('Error: ' + (res.error || 'Unknown error')); return; } const grid = $('explorerGrid'); if (!grid) return; grid.innerHTML = ''; if (!res.items || res.items.length === 0) { grid.innerHTML = '
Leerer Ordner
'; setStatus('Empty folder'); return; } res.items.forEach(item => { const icon = item.type === 'dir' ? '📁' : '📄'; const card = document.createElement('div'); card.className = 'item-card'; card.innerHTML = `
${icon}
${item.name}
`; // Drag für Files und Folders if (item.type === 'dir') { card.draggable = true; card.addEventListener('dragstart', async (ev) => { ev.preventDefault(); showProgress(0, `Preparing ${item.name}...`); try { const resDrag = await window.electronAPI.prepareDownloadDrag({ owner, repo, path: item.path }); if (resDrag.ok) { window.electronAPI.startNativeDrag(resDrag.tempPath); } } catch (error) { console.error('Drag error:', error); } finally { hideProgress(); } }); } // Drop in Ordner card.addEventListener('dragover', (ev) => { ev.preventDefault(); ev.stopPropagation(); if (item.type === 'dir') card.classList.add('drag-target'); }); card.addEventListener('dragleave', (ev) => { ev.preventDefault(); ev.stopPropagation(); card.classList.remove('drag-target'); }); card.addEventListener('drop', async (ev) => { ev.preventDefault(); ev.stopPropagation(); card.classList.remove('drag-target'); if (item.type !== 'dir') return; const files = ev.dataTransfer.files; if (!files || files.length === 0) return; const paths = Array.from(files).map(f => f.path); const targetPath = item.path; for (const p of paths) { const baseName = p.split(/[\\/]/).pop(); showProgress(0, `Uploading ${baseName} to ${targetPath}...`); try { await window.electronAPI.uploadAndPush({ localFolder: p, owner, repo, destPath: targetPath, branch: 'main' }); } catch (error) { console.error('Upload error:', error); } } hideProgress(); loadRepoContents(owner, repo, path); }); if (item.type === 'dir') { card.onclick = () => loadRepoContents(owner, repo, item.path); } else { card.onclick = () => openGiteaFileInEditor(owner, repo, item.path, item.name); } card.oncontextmenu = (ev) => showGiteaItemContextMenu(ev, item, owner, repo); grid.appendChild(card); }); setStatus(`Loaded ${res.items.length} items`); } catch (error) { console.error('Error loading repo contents:', error); setStatus('Error loading contents'); } } /* ------------------------- LOKALE LOGIK ------------------------- */ async function selectLocalFolder() { try { const folder = await window.electronAPI.selectFolder(); if (!folder) return; selectedFolder = folder; setStatus('Local: ' + folder); currentState.view = 'local'; updateNavigationUI(); await refreshLocalTree(folder); await loadBranches(folder); } catch (error) { console.error('Error selecting folder:', error); setStatus('Error selecting folder'); } } async function refreshLocalTree(folder) { try { const res = await window.electronAPI.getFileTree({ folder, exclude: ['node_modules', '.git'], maxDepth: 5 }); const grid = $('explorerGrid'); if (!grid) return; grid.innerHTML = ''; if (!res.ok) { setStatus('Error loading local files'); return; } if (!res.tree || res.tree.length === 0) { grid.innerHTML = '
Keine Dateien gefunden
'; return; } res.tree.forEach(node => { const card = document.createElement('div'); card.className = 'item-card'; card.innerHTML = `
${node.isDirectory ? '📁' : '📄'}
${node.name}
`; card.onclick = async () => { if (!node.isDirectory) { openFileEditor(node.path, node.name); } }; grid.appendChild(card); }); } catch (error) { console.error('Error refreshing tree:', error); setStatus('Error loading file tree'); } } /* ------------------------- GIT ACTIONS ------------------------- */ async function pushLocalFolder() { if (!selectedFolder) { alert('Select local folder first'); return; } const branch = $('branchSelect')?.value || 'main'; const repoName = $('repoName')?.value; const platform = $('platform')?.value; setStatus('Pushing...'); showProgress(0, 'Starting push...'); try { const res = await window.electronAPI.pushProject({ folder: selectedFolder, branch, repoName, platform }); if (res.ok) { setStatus('Push succeeded'); } else { setStatus('Push failed: ' + (res.error || 'Unknown error')); } } catch (error) { console.error('Push error:', error); setStatus('Push failed'); } finally { hideProgress(); } } async function loadBranches(folder) { try { const res = await window.electronAPI.getBranches({ folder }); const sel = $('branchSelect'); if (!sel) return; sel.innerHTML = ''; if (res.ok && res.branches) { res.branches.forEach(b => { const option = document.createElement('option'); option.value = b; option.textContent = b; sel.appendChild(option); }); } } catch (error) { console.error('Error loading branches:', error); } } async function loadCommitLogs(folder) { try { const res = await window.electronAPI.getCommitLogs({ folder }); const container = $('logs'); if (!container) return; container.innerHTML = ''; if (res.ok && res.logs) { res.logs.forEach(l => { const d = document.createElement('div'); d.className = 'log-item'; d.innerText = l; container.appendChild(d); }); } } catch (error) { console.error('Error loading logs:', error); } } /* ------------------------- CONTEXT MENÜS ------------------------- */ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) { ev.preventDefault(); ev.stopPropagation(); const old = $('ctxMenu'); if (old) old.remove(); const menu = document.createElement('div'); menu.id = 'ctxMenu'; menu.className = 'context-menu'; menu.style.left = ev.clientX + 'px'; menu.style.top = ev.clientY + 'px'; const createMenuItem = (icon, text, onClick, color = null) => { const item = document.createElement('div'); item.className = 'context-item'; item.innerHTML = `${icon} ${text}`; if (color) item.style.color = color; item.onclick = onClick; return item; }; const uploadItem = createMenuItem('🚀', 'Folder hier hochladen', async () => { menu.remove(); try { const sel = await window.electronAPI.selectFolder(); if (sel) { showProgress(0, 'Upload...'); await window.electronAPI.uploadAndPush({ localFolder: sel, owner, repo: repoName, destPath: '', cloneUrl, branch: 'main' }); hideProgress(); setStatus('Upload complete'); } } catch (error) { console.error('Upload error:', error); hideProgress(); setStatus('Upload failed'); } }); const deleteItem = createMenuItem('🗑️', 'Repo löschen', async () => { menu.remove(); if (confirm(`Delete ${repoName}?`)) { try { const res = await window.electronAPI.deleteGiteaRepo({ owner, repo: repoName }); if (res.ok) { element.remove(); setStatus('Repository deleted'); } else { setStatus('Delete failed: ' + res.error); } } catch (error) { console.error('Delete error:', error); setStatus('Delete failed'); } } }, '#ef4444'); menu.appendChild(uploadItem); menu.appendChild(deleteItem); document.body.appendChild(menu); setTimeout(() => { document.addEventListener('click', () => menu.remove(), { once: true }); }, 10); } function showGiteaItemContextMenu(ev, item, owner, repo) { ev.preventDefault(); ev.stopPropagation(); const old = $('ctxMenu'); if (old) old.remove(); const menu = document.createElement('div'); menu.id = 'ctxMenu'; menu.className = 'context-menu'; menu.style.left = ev.clientX + 'px'; menu.style.top = ev.clientY + 'px'; if (item.type === 'file') { const downloadItem = document.createElement('div'); downloadItem.className = 'context-item'; downloadItem.innerHTML = '📥 Download File'; downloadItem.onclick = async () => { menu.remove(); try { const res = await window.electronAPI.downloadGiteaFile({ owner, repo, path: item.path }); if (res.ok) { setStatus(`Saved to ${res.savedTo}`); } else { setStatus('Download failed'); } } catch (error) { console.error('Download error:', error); setStatus('Download failed'); } }; menu.appendChild(downloadItem); } document.body.appendChild(menu); setTimeout(() => { document.addEventListener('click', () => menu.remove(), { once: true }); }, 10); } /* ------------------------- HELPER FUNCTIONS ------------------------- */ function ppathBasename(p) { try { return p.split(/[\\/]/).pop(); } catch (_) { return p; } } async function previewGiteaFile(owner, repo, filePath) { try { const res = await window.electronAPI.getGiteaFileContent({ owner, repo, path: filePath, ref: 'main' }); if (res.ok) { console.log("Content of", filePath, ":", res.content); setStatus(`Previewed: ${filePath}`); } else { setStatus('Preview failed'); } } catch (error) { console.error('Preview error:', error); setStatus('Preview failed'); } } async function createRepoHandler() { const name = $('repoName')?.value?.trim(); if (!name) { alert('Name required'); return; } setStatus('Creating repository...'); try { const res = await window.electronAPI.createRepo({ name, platform: $('platform').value, license: $('licenseSelect')?.value || '', autoInit: $('createReadme')?.checked || true }); if (res.ok) { $('repoActionModal')?.classList.add('hidden'); setStatus('Repository created'); loadGiteaRepos(); } else { setStatus('Create failed: ' + (res.error || 'Unknown error')); } } catch (error) { console.error('Create repo error:', error); setStatus('Create failed'); } } /* ------------------------- GLOBALER DROP-HANDLER FÜR REPO-ANSICHT ------------------------- */ function setupGlobalDropZone() { const main = $('main'); if (!main) return; // Visual feedback beim Drag über das Fenster let dragCounter = 0; main.addEventListener('dragenter', (ev) => { // Nur in Repo-Ansicht aktiv if (currentState.view !== 'gitea-repo') return; dragCounter++; if (dragCounter === 1) { main.classList.add('drop-active'); } }); main.addEventListener('dragleave', (ev) => { if (currentState.view !== 'gitea-repo') return; dragCounter--; if (dragCounter === 0) { main.classList.remove('drop-active'); } }); main.addEventListener('dragover', (ev) => { if (currentState.view !== 'gitea-repo') return; ev.preventDefault(); }); main.addEventListener('drop', async (ev) => { if (currentState.view !== 'gitea-repo') return; ev.preventDefault(); ev.stopPropagation(); dragCounter = 0; main.classList.remove('drop-active'); const files = ev.dataTransfer.files; if (!files || files.length === 0) { setStatus("Keine Dateien zum Upload gefunden."); return; } // Upload in aktuellen Pfad const owner = currentState.owner; const repo = currentState.repo; const targetPath = currentState.path || ''; const paths = Array.from(files).map(f => f.path); setStatus(`Uploading ${paths.length} items to /${targetPath || 'root'}...`); for (const p of paths) { const baseName = p.split(/[\\/]/).pop(); showProgress(0, `Uploading: ${baseName}`); try { const res = await window.electronAPI.uploadAndPush({ localFolder: p, owner, repo, destPath: targetPath, branch: 'main' }); if (!res.ok) { console.error("Upload error:", res.error); setStatus("Error: " + res.error); } else { setStatus(`Uploaded: ${baseName}`); } } catch (err) { console.error("Critical upload error:", err); setStatus("Upload failed"); } } hideProgress(); // Refresh current view setTimeout(() => { loadRepoContents(owner, repo, targetPath); }, 1000); }); } /* ------------------------- INITIALISIERUNG ------------------------- */ window.addEventListener('DOMContentLoaded', async () => { // Prevent default drag/drop on document (except in repo view) document.addEventListener('dragover', e => { if (currentState.view !== 'gitea-repo') { e.preventDefault(); } }); document.addEventListener('drop', e => { if (currentState.view !== 'gitea-repo') { e.preventDefault(); } }); // Load credentials try { const creds = await window.electronAPI.loadCredentials(); if (creds) { if ($('githubToken')) $('githubToken').value = creds.githubToken || ''; if ($('giteaToken')) $('giteaToken').value = creds.giteaToken || ''; if ($('giteaURL')) $('giteaURL').value = creds.giteaURL || ''; } } catch (error) { console.error('Error loading credentials:', error); } // Event Handlers if ($('btnLoadGiteaRepos')) { $('btnLoadGiteaRepos').onclick = loadGiteaRepos; } if ($('btnSelectFolder')) { $('btnSelectFolder').onclick = selectLocalFolder; } if ($('btnPush')) { $('btnPush').onclick = pushLocalFolder; } if ($('btnCreateRepo')) { $('btnCreateRepo').onclick = createRepoHandler; } if ($('btnBack')) { $('btnBack').onclick = () => { if (currentState.view === 'gitea-repo') { if (currentState.path === '' || currentState.path === '/') { loadGiteaRepos(); } else { const parts = currentState.path.split('/').filter(p => p); parts.pop(); loadRepoContents(currentState.owner, currentState.repo, parts.join('/')); } } }; } // Modal controls if ($('btnSettings')) { $('btnSettings').onclick = () => { $('settingsModal').classList.remove('hidden'); }; } if ($('btnCloseSettings')) { $('btnCloseSettings').onclick = () => { $('settingsModal').classList.add('hidden'); }; } if ($('btnOpenRepoActions')) { $('btnOpenRepoActions').onclick = () => { $('repoActionModal').classList.remove('hidden'); }; } if ($('btnCloseRepoActions')) { $('btnCloseRepoActions').onclick = () => { $('repoActionModal').classList.add('hidden'); }; } if ($('btnSaveSettings')) { $('btnSaveSettings').onclick = async () => { try { const data = { githubToken: $('githubToken').value, giteaToken: $('giteaToken').value, giteaURL: $('giteaURL').value }; await window.electronAPI.saveCredentials(data); $('settingsModal').classList.add('hidden'); setStatus('Settings saved'); } catch (error) { console.error('Error saving settings:', error); setStatus('Save failed'); } }; } // FILE EDITOR EVENT LISTENERS if ($('btnCloseEditor')) { $('btnCloseEditor').onclick = closeFileEditor; } if ($('btnEditorSave')) { $('btnEditorSave').onclick = () => saveCurrentFile(false); } if ($('btnEditorSearch')) { $('btnEditorSearch').onclick = toggleSearch; } if ($('btnReplace')) { $('btnReplace').onclick = replaceOnce; } if ($('btnReplaceAll')) { $('btnReplaceAll').onclick = replaceAll; } if ($('btnCloseSearch')) { $('btnCloseSearch').onclick = () => { $('searchBar').classList.add('hidden'); }; } if ($('searchInput')) { $('searchInput').addEventListener('input', performSearch); $('searchInput').addEventListener('keydown', (e) => { if (e.key === 'Enter') { performSearch(); } }); } if ($('btnDiscardEdit')) { $('btnDiscardEdit').onclick = () => { const tab = openTabs[currentActiveTab]; if (tab) { tab.content = tab.originalContent; tab.dirty = false; tab.history = [tab.originalContent]; tab.historyIndex = 0; updateEditor(); } }; } // Modal wird mit pointer-events: none nicht geschlossen durch Klicks // Der Grid bleibt voll interaktiv für neue Tabs // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.ctrlKey || e.metaKey) { // Ctrl+S - Save if (e.key === 's') { e.preventDefault(); if (currentActiveTab) { saveCurrentFile(false); } } // Ctrl+F - Search if (e.key === 'f') { e.preventDefault(); if (currentActiveTab) { toggleSearch(); } } // Ctrl+H - Replace if (e.key === 'h') { e.preventDefault(); if (currentActiveTab) { toggleSearch(); $('replaceInput').focus(); } } } // ESC - Close search if (e.key === 'Escape') { if (!$('searchBar').classList.contains('hidden')) { $('searchBar').classList.add('hidden'); } } }); // Progress listeners window.electronAPI.onFolderUploadProgress(p => { showProgress(p.percent, `Upload: ${p.processed}/${p.total}`); }); window.electronAPI.onFolderDownloadProgress(p => { showProgress(p.percent, `Download: ${p.processed}/${p.total}`); }); // Setup globalen Drop-Handler für Repo-Ansicht setupGlobalDropZone(); setStatus('Ready'); initUpdater(); // Updater initialisieren updateNavigationUI(); }); /* ================================ RELEASE MANAGEMENT UI FUNCTIONS Füge dies zu renderer.js hinzu ================================ */ let currentReleaseView = { owner: null, repo: null }; /* ------------------------- RELEASES LADEN & ANZEIGEN ------------------------- */ async function loadRepoReleases(owner, repo) { currentReleaseView.owner = owner; currentReleaseView.repo = repo; setStatus('Loading releases...'); try { const res = await window.electronAPI.listReleases({ owner, repo }); if (!res.ok) { setStatus('Error loading releases: ' + res.error); return; } const grid = $('explorerGrid'); if (!grid) return; // Header mit "New Release" Button grid.innerHTML = `

📦 Releases für ${repo}

`; // Event-Listener MUSS VOR innerHTML += gesetzt werden const newBtn = grid.querySelector('.btn-new-release'); if (newBtn) { newBtn.onclick = () => { console.log('New Release button clicked'); showCreateReleaseModal(owner, repo); }; } else { console.error('New Release button not found in DOM'); } if (!res.releases || res.releases.length === 0) { // WICHTIG: appendChild statt innerHTML +=, um Event-Listener zu erhalten const emptyMsg = document.createElement('div'); emptyMsg.style.cssText = 'grid-column: 1/-1; text-align: center; padding: 60px; color: var(--text-muted); font-size: 16px;'; emptyMsg.textContent = '📭 Noch keine Releases veröffentlicht'; grid.appendChild(emptyMsg); setStatus('No releases'); return; } // Releases als Cards darstellen res.releases.forEach((release, index) => { const card = createReleaseCard(release, index === 0); grid.appendChild(card); }); setStatus(`${res.releases.length} release(s) loaded`); } catch (error) { console.error('Error loading releases:', error); setStatus('Failed to load releases'); } } function createReleaseCard(release, isLatest) { const card = document.createElement('div'); card.className = 'release-card'; card.style.cssText = ` grid-column: 1/-1; background: var(--bg-tertiary); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: var(--radius-lg); padding: var(--spacing-xl); margin-bottom: var(--spacing-lg); transition: all var(--transition-normal); `; // Header mit Tag und Badges const header = document.createElement('div'); header.style.cssText = 'display: flex; gap: 10px; align-items: center; margin-bottom: 12px;'; const tag = document.createElement('span'); tag.textContent = release.tag_name; tag.style.cssText = ` background: var(--accent-gradient); color: #000; padding: 6px 16px; border-radius: 20px; font-weight: 700; font-size: 14px; `; header.appendChild(tag); if (isLatest) { const latestBadge = document.createElement('span'); latestBadge.textContent = 'LATEST'; latestBadge.style.cssText = ` background: var(--success); color: #000; padding: 4px 12px; border-radius: 12px; font-size: 11px; font-weight: 700; `; header.appendChild(latestBadge); } if (release.prerelease) { const preBadge = document.createElement('span'); preBadge.textContent = 'PRE-RELEASE'; preBadge.style.cssText = ` background: var(--warning); color: #000; padding: 4px 12px; border-radius: 12px; font-size: 11px; font-weight: 700; `; header.appendChild(preBadge); } if (release.draft) { const draftBadge = document.createElement('span'); draftBadge.textContent = 'DRAFT'; draftBadge.style.cssText = ` background: rgba(255, 255, 255, 0.2); color: var(--text-primary); padding: 4px 12px; border-radius: 12px; font-size: 11px; font-weight: 700; `; header.appendChild(draftBadge); } card.appendChild(header); // Title const title = document.createElement('h3'); title.textContent = release.name || release.tag_name; title.style.cssText = 'margin: 0 0 12px 0; color: var(--text-primary); font-size: 20px;'; card.appendChild(title); // Body (Release Notes) if (release.body) { const body = document.createElement('div'); body.className = 'release-body'; body.innerHTML = parseMarkdownToHTML(release.body); card.appendChild(body); } // Assets if (release.assets && release.assets.length > 0) { const assetsContainer = document.createElement('div'); assetsContainer.style.cssText = ` margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(255, 255, 255, 0.05); `; const assetsTitle = document.createElement('div'); assetsTitle.textContent = '📦 Assets'; assetsTitle.style.cssText = 'font-weight: 600; margin-bottom: 12px; color: var(--text-primary);'; assetsContainer.appendChild(assetsTitle); release.assets.forEach(asset => { const assetItem = document.createElement('div'); assetItem.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 10px; background: rgba(255, 255, 255, 0.03); border-radius: var(--radius-sm); margin-bottom: 8px; `; const assetName = document.createElement('span'); assetName.textContent = `📎 ${asset.name}`; assetName.style.cssText = 'color: var(--text-primary);'; const assetSize = document.createElement('span'); assetSize.textContent = formatBytes(asset.size || 0); assetSize.style.cssText = 'color: var(--text-muted); font-size: 12px; margin-left: 12px;'; const downloadBtn = document.createElement('button'); downloadBtn.textContent = '⬇️ Download'; downloadBtn.style.cssText = ` background: var(--bg-secondary); color: var(--text-primary); border: 1px solid rgba(255, 255, 255, 0.1); padding: 6px 12px; border-radius: var(--radius-sm); cursor: pointer; font-size: 12px; `; downloadBtn.onclick = () => { if (asset.browser_download_url) { window.open(asset.browser_download_url, '_blank'); } }; const deleteAssetBtn = document.createElement('button'); deleteAssetBtn.textContent = '🗑️'; deleteAssetBtn.style.cssText = ` background: transparent; color: var(--danger); border: 1px solid var(--danger); padding: 6px 10px; border-radius: var(--radius-sm); cursor: pointer; font-size: 12px; margin-left: 8px; `; deleteAssetBtn.onclick = async () => { if (confirm(`Delete asset "${asset.name}"?`)) { const res = await window.electronAPI.deleteReleaseAsset({ owner: currentReleaseView.owner, repo: currentReleaseView.repo, assetId: asset.id }); if (res.ok) { assetItem.remove(); setStatus('Asset deleted'); } } }; const leftSide = document.createElement('div'); leftSide.style.cssText = 'display: flex; align-items: center; gap: 12px;'; leftSide.appendChild(assetName); leftSide.appendChild(assetSize); const rightSide = document.createElement('div'); rightSide.style.cssText = 'display: flex; gap: 8px;'; rightSide.appendChild(downloadBtn); rightSide.appendChild(deleteAssetBtn); assetItem.appendChild(leftSide); assetItem.appendChild(rightSide); assetsContainer.appendChild(assetItem); }); card.appendChild(assetsContainer); } // Meta Info const meta = document.createElement('div'); meta.style.cssText = ` display: flex; gap: 20px; margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(255, 255, 255, 0.05); color: var(--text-muted); font-size: 12px; `; const date = new Date(release.created_at); const dateStr = date.toLocaleDateString('de-DE', { year: 'numeric', month: 'long', day: 'numeric' }); meta.innerHTML = ` 📅 ${dateStr} 👤 ${release.author?.login || 'Unknown'} `; card.appendChild(meta); // Action Buttons const actions = document.createElement('div'); actions.style.cssText = 'display: flex; gap: 12px; margin-top: 16px;'; const downloadArchiveBtn = document.createElement('button'); downloadArchiveBtn.textContent = '📦 Download ZIP'; downloadArchiveBtn.style.cssText = ` background: var(--bg-secondary); color: var(--text-primary); border: 1px solid rgba(255, 255, 255, 0.1); padding: 8px 16px; border-radius: var(--radius-md); cursor: pointer; font-weight: 600; `; downloadArchiveBtn.onclick = async () => { const res = await window.electronAPI.downloadReleaseArchive({ owner: currentReleaseView.owner, repo: currentReleaseView.repo, tag: release.tag_name }); if (res.ok) { setStatus(`Downloaded to ${res.savedTo}`); } }; const addAssetBtn = document.createElement('button'); addAssetBtn.textContent = '📎 Add Asset'; addAssetBtn.style.cssText = ` background: var(--bg-secondary); color: var(--text-primary); border: 1px solid rgba(255, 255, 255, 0.1); padding: 8px 16px; border-radius: var(--radius-md); cursor: pointer; font-weight: 600; `; addAssetBtn.onclick = () => showUploadAssetDialog(release); const deleteBtn = document.createElement('button'); deleteBtn.textContent = '🗑️ Delete'; deleteBtn.style.cssText = ` background: transparent; color: var(--danger); border: 1px solid var(--danger); padding: 8px 16px; border-radius: var(--radius-md); cursor: pointer; font-weight: 600; margin-left: auto; `; deleteBtn.onclick = async () => { if (confirm(`Delete release "${release.name || release.tag_name}"?`)) { const res = await window.electronAPI.deleteRelease({ owner: currentReleaseView.owner, repo: currentReleaseView.repo, releaseId: release.id }); if (res.ok) { card.remove(); setStatus('Release deleted'); } } }; actions.appendChild(downloadArchiveBtn); actions.appendChild(addAssetBtn); actions.appendChild(deleteBtn); card.appendChild(actions); return card; } /* ------------------------- CREATE RELEASE MODAL ------------------------- */ function showCreateReleaseModal(owner, repo) { let selectedFiles = []; const modal = document.createElement('div'); modal.className = 'modal'; modal.innerHTML = `

🚀 Neues Release erstellen

`; document.body.appendChild(modal); // Asset-Auswahl Handler $('btnSelectAssets').onclick = async () => { const res = await window.electronAPI.selectFile(); if (res.ok && res.files && res.files.length > 0) { selectedFiles = res.files; updateAssetsList(); } }; function updateAssetsList() { const list = $('assetsList'); if (selectedFiles.length === 0) { list.innerHTML = ''; return; } list.innerHTML = selectedFiles.map((file, idx) => `
📄 ${file.name}
`).join(''); } // Asset entfernen (global für onclick) window.removeAsset = (idx) => { selectedFiles.splice(idx, 1); updateAssetsList(); }; $('btnCreateRelease').onclick = async () => { const tag = $('releaseTag').value.trim(); const name = $('releaseName').value.trim() || tag; const body = $('releaseBody').value.trim(); const target = $('releaseTarget').value.trim() || 'main'; const prerelease = $('releasePrerelease').checked; const draft = $('releaseDraft').checked; if (!tag) { alert('Tag Version ist erforderlich!'); return; } const btnCreate = $('btnCreateRelease'); const originalText = btnCreate.textContent; btnCreate.disabled = true; btnCreate.textContent = 'Erstelle Release...'; setStatus('Creating release...'); try { const res = await window.electronAPI.createRelease({ owner, repo, tag_name: tag, name, body, target_commitish: target, prerelease, draft }); if (res.ok) { // Assets hochladen, falls vorhanden if (selectedFiles.length > 0) { btnCreate.textContent = `Lade Assets (0/${selectedFiles.length})...`; setStatus(`Uploading ${selectedFiles.length} asset(s)...`); for (let i = 0; i < selectedFiles.length; i++) { const file = selectedFiles[i]; btnCreate.textContent = `Lade Assets (${i + 1}/${selectedFiles.length})...`; try { await window.electronAPI.uploadReleaseAsset({ owner, repo, releaseId: res.release.id, filePath: file.path, fileName: file.name }); } catch (err) { console.error('Asset upload error:', err); // Weiter mit nächster Datei } } } modal.remove(); setStatus('Release created successfully!'); loadRepoReleases(owner, repo); // Reload } else { setStatus('Failed: ' + res.error); btnCreate.disabled = false; btnCreate.textContent = originalText; } } catch (error) { console.error('Create release error:', error); setStatus('Create failed'); btnCreate.disabled = false; btnCreate.textContent = originalText; } }; $('btnCancelRelease').onclick = () => modal.remove(); // Close on background click modal.onclick = (e) => { if (e.target === modal) modal.remove(); }; } /* ------------------------- UPLOAD ASSET DIALOG ------------------------- */ async function showUploadAssetDialog(release) { try { const res = await window.electronAPI.selectFile(); if (!res.ok || !res.files || res.files.length === 0) { return; } const file = res.files[0]; const filePath = file.path; const fileName = file.name; setStatus(`Uploading ${fileName}...`); showProgress(0, `Uploading ${fileName}...`); const uploadRes = await window.electronAPI.uploadReleaseAsset({ owner: currentReleaseView.owner, repo: currentReleaseView.repo, releaseId: release.id, filePath, fileName }); hideProgress(); if (uploadRes.ok) { setStatus('Asset uploaded!'); // Reload releases to show new asset loadRepoReleases(currentReleaseView.owner, currentReleaseView.repo); } else { setStatus('Upload failed: ' + uploadRes.error); } } catch (error) { console.error('Upload asset error:', error); hideProgress(); setStatus('Upload failed'); } } /* ------------------------- HELPER FUNCTIONS ------------------------- */ function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; } /* ------------------------- INTEGRATION IN REPO VIEW Füge "Releases" Tab zum Repo hinzu ------------------------- */ // Modifiziere die loadRepoContents Funktion um einen Releases-Button hinzuzufügen: // Nach dem Laden eines Repos, zeige einen Button "View Releases" an /* ================================ COMMIT HISTORY VISUALIZATION UI Füge dies zu renderer.js hinzu ================================ */ let currentCommitView = { owner: null, repo: null, branch: 'main', commits: [], selectedCommit: null }; /* ------------------------- COMMIT HISTORY LADEN ------------------------- */ async function loadCommitHistory(owner, repo, branch = 'main') { currentCommitView.owner = owner; currentCommitView.repo = repo; currentCommitView.branch = branch; setStatus('Loading commit history...'); try { const res = await window.electronAPI.getCommits({ owner, repo, branch, limit: 100 }); if (!res.ok) { setStatus('Error loading commits: ' + res.error); return; } currentCommitView.commits = res.commits; renderCommitHistoryView(); setStatus(`${res.commits.length} commits loaded`); } catch (error) { console.error('Error loading commit history:', error); setStatus('Failed to load commits'); } } function renderCommitHistoryView() { const grid = $('explorerGrid'); if (!grid) return; grid.innerHTML = ''; grid.style.gridTemplateColumns = '1fr'; // Header mit Search und Branch-Selector const header = document.createElement('div'); header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; gap: 16px; flex-wrap: wrap; `; header.innerHTML = `

📊 Commit History ${currentCommitView.repo} / ${currentCommitView.branch}

`; grid.appendChild(header); // Search-Handler const searchInput = header.querySelector('#commitSearch'); const clearBtn = header.querySelector('#btnClearSearch'); let searchTimeout; searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { handleCommitSearch(e.target.value); }, 300); }); clearBtn.onclick = () => { searchInput.value = ''; renderCommitTimeline(currentCommitView.commits); }; // Timeline Container const timelineContainer = document.createElement('div'); timelineContainer.id = 'commitTimeline'; timelineContainer.style.cssText = ` position: relative; max-width: 100%; `; grid.appendChild(timelineContainer); // Initial render renderCommitTimeline(currentCommitView.commits); } function renderCommitTimeline(commits) { const container = $('commitTimeline'); if (!container) return; container.innerHTML = ''; if (!commits || commits.length === 0) { container.innerHTML = '
📭 No commits found
'; return; } // Timeline mit Cards commits.forEach((commit, index) => { const card = createCommitCard(commit, index); container.appendChild(card); }); } function createCommitCard(commit, index) { const card = document.createElement('div'); card.className = 'commit-card'; card.dataset.sha = commit.sha; const isEven = index % 2 === 0; card.style.cssText = ` position: relative; padding-left: 60px; margin-bottom: 32px; cursor: pointer; transition: all var(--transition-normal); `; // Timeline dot const dot = document.createElement('div'); dot.style.cssText = ` position: absolute; left: 18px; top: 0; width: 16px; height: 16px; background: var(--accent-primary); border: 3px solid var(--bg-primary); border-radius: 50%; z-index: 2; box-shadow: 0 0 0 4px var(--bg-tertiary); `; card.appendChild(dot); // Timeline line if (index < currentCommitView.commits.length - 1) { const line = document.createElement('div'); line.style.cssText = ` position: absolute; left: 25px; top: 16px; width: 2px; height: calc(100% + 32px); background: linear-gradient(180deg, var(--accent-primary) 0%, rgba(0, 212, 255, 0.2) 100%); z-index: 1; `; card.appendChild(line); } // Content card const content = document.createElement('div'); content.className = 'commit-content'; content.style.cssText = ` background: var(--bg-tertiary); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: var(--radius-lg); padding: var(--spacing-lg); transition: all var(--transition-normal); `; // Commit message const message = commit.commit?.message || commit.message || 'No message'; const shortMessage = message.split('\n')[0]; // First line only const messageEl = document.createElement('div'); messageEl.style.cssText = ` font-size: 16px; font-weight: 600; color: var(--text-primary); margin-bottom: 12px; line-height: 1.4; `; messageEl.textContent = shortMessage; content.appendChild(messageEl); // Meta info const meta = document.createElement('div'); meta.style.cssText = ` display: flex; gap: 20px; flex-wrap: wrap; font-size: 13px; color: var(--text-muted); margin-bottom: 12px; `; const author = commit.commit?.author?.name || commit.author?.login || 'Unknown'; const date = new Date(commit.commit?.author?.date || commit.created_at); const dateStr = formatRelativeTime(date); const sha = commit.sha?.substring(0, 7) || '???????'; meta.innerHTML = ` 👤 ${author} 🕐 ${dateStr} #${sha} `; content.appendChild(meta); // Stats (if available) if (commit.stats) { const stats = document.createElement('div'); stats.style.cssText = ` display: flex; gap: 16px; font-size: 12px; padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, 0.05); `; stats.innerHTML = ` +${commit.stats.additions || 0} -${commit.stats.deletions || 0} ${commit.stats.total || 0} changes `; content.appendChild(stats); } card.appendChild(content); // Hover effect card.addEventListener('mouseenter', () => { content.style.borderColor = 'var(--accent-primary)'; content.style.transform = 'translateX(4px)'; content.style.boxShadow = 'var(--shadow-md)'; }); card.addEventListener('mouseleave', () => { content.style.borderColor = 'rgba(255, 255, 255, 0.1)'; content.style.transform = 'translateX(0)'; content.style.boxShadow = 'none'; }); // Click to show details card.onclick = () => showCommitDetails(commit); return card; } /* ------------------------- COMMIT DETAILS & DIFF VIEWER ------------------------- */ async function showCommitDetails(commit) { currentCommitView.selectedCommit = commit; const modal = document.createElement('div'); modal.className = 'modal commit-modal'; modal.innerHTML = `

📋 Commit Details

Loading commit details...
`; document.body.appendChild(modal); $('btnCloseCommitModal').onclick = () => modal.remove(); modal.onclick = (e) => { if (e.target === modal) modal.remove(); }; // Load commit details await loadCommitDetailsContent(commit); } async function loadCommitDetailsContent(commit) { const container = $('commitDetailsContent'); if (!container) return; try { // Check if this is local git or Gitea repo let diffRes, filesRes; if (selectedFolder) { // Local Git repository const details = await window.electronAPI.getLocalCommitDetails({ folderPath: selectedFolder, sha: commit.sha || commit.hash }); diffRes = { ok: true, diff: details?.diff || '' }; filesRes = { ok: true, files: details?.fileChanges?.files || [], stats: { additions: details?.fileChanges?.insertions || 0, deletions: details?.fileChanges?.deletions || 0 } }; } else { // Gitea repository const [diff, files] = await Promise.all([ window.electronAPI.getCommitDiff({ owner: currentCommitView.owner, repo: currentCommitView.repo, sha: commit.sha }), window.electronAPI.getCommitFiles({ owner: currentCommitView.owner, repo: currentCommitView.repo, sha: commit.sha }) ]); diffRes = diff; filesRes = files; } container.innerHTML = ''; // Commit info header const header = document.createElement('div'); header.style.cssText = ` background: var(--bg-tertiary); padding: var(--spacing-xl); border-radius: var(--radius-lg); margin-bottom: 24px; `; const message = commit.commit?.message || commit.message || 'No message'; const author = commit.commit?.author?.name || commit.author?.login || 'Unknown'; const email = commit.commit?.author?.email || ''; const date = new Date(commit.commit?.author?.date || commit.created_at); const sha = commit.sha || ''; header.innerHTML = `

${escapeHtml(message)}

👤 ${escapeHtml(author)} ${email ? `<${escapeHtml(email)}>` : ''} 🕐 ${date.toLocaleString()} ${sha.substring(0, 7)}
`; container.appendChild(header); // File changes summary if (filesRes.ok && filesRes.files && Array.isArray(filesRes.files) && filesRes.files.length > 0) { const filesHeader = document.createElement('div'); filesHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; `; filesHeader.innerHTML = `

📁 Changed Files (${filesRes.files.length})

+${filesRes.stats?.additions || 0} -${filesRes.stats?.deletions || 0}
`; container.appendChild(filesHeader); // File list const fileList = document.createElement('div'); fileList.style.cssText = 'margin-bottom: 24px;'; filesRes.files.forEach(file => { const fileItem = document.createElement('div'); fileItem.style.cssText = ` display: flex; justify-content: space-between; padding: 8px 12px; background: rgba(255, 255, 255, 0.03); border-radius: var(--radius-sm); margin-bottom: 4px; font-size: 13px; `; const changeType = file.changes === file.insertions ? 'added' : file.changes === file.deletions ? 'deleted' : 'modified'; const icon = changeType === 'added' ? '🆕' : changeType === 'deleted' ? '🗑️' : '📝'; fileItem.innerHTML = ` ${icon} ${escapeHtml(file.file)} +${file.insertions} -${file.deletions} `; fileList.appendChild(fileItem); }); container.appendChild(fileList); } // Diff viewer if (diffRes.ok && diffRes.diff) { const diffHeader = document.createElement('h4'); diffHeader.textContent = '📝 Changes (Diff)'; diffHeader.style.marginBottom = '12px'; container.appendChild(diffHeader); const diffContainer = document.createElement('div'); diffContainer.className = 'diff-viewer'; diffContainer.style.cssText = ` background: #1e1e1e; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: var(--radius-md); padding: 16px; overflow-x: auto; font-family: 'Courier New', monospace; font-size: 13px; line-height: 1.6; max-height: 600px; overflow-y: auto; `; diffContainer.innerHTML = formatDiff(diffRes.diff); container.appendChild(diffContainer); } } catch (error) { console.error('Error loading commit details:', error); console.error('Stack:', error.stack); console.error('selectedFolder:', selectedFolder); console.error('commit:', commit); const errorMsg = error.message || String(error); const isLocalGit = selectedFolder ? 'Local Git' : 'Gitea'; container.innerHTML = `

❌ Error loading commit details

Source: ${isLocalGit}
Error: ${escapeHtml(errorMsg)}

`; } } function formatDiff(diffText) { const lines = diffText.split('\n'); let html = ''; lines.forEach(line => { let color = '#d4d4d4'; let bgColor = 'transparent'; if (line.startsWith('+++') || line.startsWith('---')) { color = '#569cd6'; // Blue } else if (line.startsWith('+')) { color = '#4ec9b0'; // Green bgColor = 'rgba(78, 201, 176, 0.1)'; } else if (line.startsWith('-')) { color = '#f48771'; // Red bgColor = 'rgba(244, 135, 113, 0.1)'; } else if (line.startsWith('@@')) { color = '#c586c0'; // Purple } else if (line.startsWith('diff')) { color = '#dcdcaa'; // Yellow } html += `
${escapeHtml(line)}
`; }); return html; } /* ------------------------- COMMIT SEARCH ------------------------- */ async function handleCommitSearch(query) { if (!query || query.trim().length === 0) { renderCommitTimeline(currentCommitView.commits); return; } setStatus('Searching commits...'); try { const res = await window.electronAPI.searchCommits({ owner: currentCommitView.owner, repo: currentCommitView.repo, branch: currentCommitView.branch, query: query.trim() }); if (res.ok) { renderCommitTimeline(res.commits); setStatus(`Found ${res.commits.length} commits`); } else { setStatus('Search failed'); } } catch (error) { console.error('Search error:', error); setStatus('Search error'); } } /* ------------------------- HELPER FUNCTIONS ------------------------- */ function formatRelativeTime(date) { const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return 'just now'; if (diffMins < 60) return `${diffMins} min ago`; if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) > 1 ? 's' : ''} ago`; if (diffDays < 365) return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) > 1 ? 's' : ''} ago`; return `${Math.floor(diffDays / 365)} year${Math.floor(diffDays / 365) > 1 ? 's' : ''} ago`; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /* ------------------------- EVENT LISTENERS ------------------------- */ // File Editor Event Listeners setTimeout(() => { // Buttons const btnClose = $('btnCloseEditor'); const btnSave = $('btnEditorSave'); const btnSearch = $('btnEditorSearch'); const btnDiscard = $('btnDiscardEdit'); const btnFileActions = $('btnFileActions'); const modal = $('fileEditorModal'); // Close button if (btnClose) btnClose.addEventListener('click', closeFileEditor); // Save button if (btnSave) btnSave.addEventListener('click', saveCurrentFile); // Search button if (btnSearch) btnSearch.addEventListener('click', toggleSearch); // Discard button if (btnDiscard) btnDiscard.addEventListener('click', closeFileEditor); // File actions menu if (btnFileActions) { btnFileActions.addEventListener('click', (e) => { const menu = $('fileActionsMenu'); if (menu) { menu.classList.toggle('hidden'); const rect = btnFileActions.getBoundingClientRect(); menu.style.top = (rect.bottom + 4) + 'px'; menu.style.right = '20px'; } }); } // Search & Replace const searchInput = $('searchInput'); if (searchInput) { searchInput.addEventListener('keyup', performSearch); searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { replaceOnce(); } }); } const btnReplace = $('btnReplace'); if (btnReplace) btnReplace.addEventListener('click', replaceOnce); const btnReplaceAll = $('btnReplaceAll'); if (btnReplaceAll) btnReplaceAll.addEventListener('click', replaceAll); const btnCloseSearch = $('btnCloseSearch'); if (btnCloseSearch) btnCloseSearch.addEventListener('click', () => { const searchBar = $('searchBar'); if (searchBar) searchBar.classList.add('hidden'); }); // Textarea events const textarea = $('fileEditorContent'); if (textarea) { textarea.addEventListener('input', () => { updateEditorContent(textarea.value); }); textarea.addEventListener('scroll', () => { const lineNumbers = $('lineNumbers'); if (lineNumbers) lineNumbers.scrollTop = textarea.scrollTop; }); textarea.addEventListener('click', updateEditorStats); textarea.addEventListener('keyup', updateEditorStats); } // Close modal on background click if (modal) { modal.addEventListener('click', (e) => { if (e.target === modal) { closeFileEditor(); } }); } console.log('✅ Advanced editor event listeners registered'); }, 100); // Global keyboard shortcuts document.addEventListener('keydown', (e) => { const modal = $('fileEditorModal'); if (!modal || modal.classList.contains('hidden')) return; // Ctrl+S / Cmd+S - Save if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); saveCurrentFile(); } // Ctrl+F / Cmd+F - Search if ((e.ctrlKey || e.metaKey) && e.key === 'f') { e.preventDefault(); toggleSearch(); } // Ctrl+H / Cmd+H - Replace if ((e.ctrlKey || e.metaKey) && e.key === 'h') { e.preventDefault(); toggleSearch(); $('replaceInput').focus(); } // ESC - Close search bar if open if (e.key === 'Escape') { const searchBar = $('searchBar'); if (searchBar && !searchBar.classList.contains('hidden')) { searchBar.classList.add('hidden'); } } }); /* ======================================== UPDATER FUNKTIONEN (Optimiert & Synchronisiert) ======================================== */ async function initUpdater() { try { const versionRes = await window.electronAPI.getAppVersion(); if (versionRes && versionRes.ok && $('appVersion')) { $('appVersion').value = versionRes.version; } } catch (error) { console.error('[Renderer] Fehler beim Laden der Version:', error); } // Manueller Check Button in Settings if ($('btnCheckUpdates')) { $('btnCheckUpdates').onclick = async () => { const btn = $('btnCheckUpdates'); const originalHTML = btn.innerHTML; btn.innerHTML = '⏳ Suche...'; btn.disabled = true; try { await window.electronAPI.checkForUpdates(); setStatus('Update-Suche abgeschlossen'); } catch (error) { setStatus('Fehler bei der Update-Prüfung'); } finally { setTimeout(() => { btn.innerHTML = originalHTML; btn.disabled = false; }, 1500); } }; } } // Event-Listener für das Update-Modal if (window.electronAPI.onUpdateAvailable) { window.electronAPI.onUpdateAvailable((info) => { const modal = $('updateModal'); const versionInfo = $('updateVersionInfo'); const changelog = $('updateChangelog'); if (versionInfo) versionInfo.innerText = `Version ${info.version} verfügbar!`; if (changelog) changelog.innerText = info.body || 'Keine Release-Notes vorhanden.'; if (modal) modal.classList.remove('hidden'); // Button: Jetzt installieren const updateBtn = $('btnStartUpdate'); if (updateBtn) { updateBtn.onclick = () => { if (modal) modal.classList.add('hidden'); setStatus('Download gestartet...'); // Aufruf der korrekten Preload-Funktion window.electronAPI.startUpdateDownload(info.asset); }; } // Button: Später const ignoreBtn = $('btnIgnoreUpdate'); if (ignoreBtn) { ignoreBtn.onclick = () => { if (modal) modal.classList.add('hidden'); }; } }); } // AM ENDE DER DATEI: Initialisierung beim Start document.addEventListener('DOMContentLoaded', () => { // 1. Basis-Setup (Settings-Feld füllen etc.) initUpdater(); // 2. AUTOMATISCHER UPDATE-CHECK BEIM START // Wir warten 3 Sekunden, damit die App in Ruhe laden kann setTimeout(() => { console.log("[Auto-Updater] Suche im Hintergrund nach Updates..."); window.electronAPI.checkForUpdates(); }, 3000); });