// renderer.js — Grid-UI + Navigation + Drag'n'Drop mit Fehlerbehandlung const $ = id => document.getElementById(id); let selectedFolder = null; let giteaCache = {}; /* ================================================ FAVORITEN & ZULETZT GEÖFFNET — State & Helpers ================================================ */ let favorites = []; // [{ owner, repo, cloneUrl, addedAt }] let recentRepos = []; // [{ owner, repo, cloneUrl, openedAt }] // Feature-Flags (aus Settings, persistent via Credentials) let featureFavorites = true; let featureRecent = true; let compactMode = false; async function loadFavoritesAndRecent() { try { const [favRes, recRes] = await Promise.all([ window.electronAPI.loadFavorites(), window.electronAPI.loadRecent() ]); if (favRes && favRes.ok) favorites = favRes.favorites || []; if (recRes && recRes.ok) recentRepos = recRes.recent || []; } catch(e) { console.error('loadFavoritesAndRecent:', e); } } function isFavorite(owner, repo) { return favorites.some(f => f.owner === owner && f.repo === repo); } async function toggleFavorite(owner, repo, cloneUrl) { if (isFavorite(owner, repo)) { favorites = favorites.filter(f => !(f.owner === owner && f.repo === repo)); } else { favorites.unshift({ owner, repo, cloneUrl, addedAt: new Date().toISOString() }); } await window.electronAPI.saveFavorites(favorites); } async function addToRecent(owner, repo, cloneUrl) { if (!featureRecent) return; recentRepos = recentRepos.filter(r => !(r.owner === owner && r.repo === repo)); recentRepos.unshift({ owner, repo, cloneUrl, openedAt: new Date().toISOString() }); recentRepos = recentRepos.slice(0, 20); await window.electronAPI.saveRecent(recentRepos); } function formatRelDate(iso) { if (!iso) return ''; const diff = Date.now() - new Date(iso).getTime(); const m = Math.floor(diff / 60000); const h = Math.floor(diff / 3600000); const d = Math.floor(diff / 86400000); if (m < 1) return 'Gerade eben'; if (m < 60) return `vor ${m} Min.`; if (h < 24) return `vor ${h} Std.`; if (d < 7) return `vor ${d} Tag${d > 1 ? 'en' : ''}`; return new Date(iso).toLocaleDateString('de-DE'); } /* Rendert Favoriten + Zuletzt-geöffnet-Bereich in ein beliebiges Container-Element */ // Collapse-Zustand (wird in Credentials persistiert) const favSectionCollapsed = { favorites: false, recent: false }; function makeFavSectionBlock(type, allRepos) { const isFav = type === 'favorites'; const icon = isFav ? '⭐' : '🕐'; const label = isFav ? 'Favoriten' : 'Zuletzt geöffnet'; const sec = document.createElement('div'); sec.style.cssText = `margin-bottom: ${isFav ? '20' : '24'}px;`; // ── Header (klickbar) ────────────────────────────── const hdr = document.createElement('div'); hdr.className = 'fav-section-header fav-section-header--toggle'; const iconEl = document.createElement('span'); iconEl.className = 'fav-section-icon'; if (isFav) iconEl.style.color = '#f59e0b'; iconEl.textContent = icon; const labelEl = document.createElement('span'); labelEl.textContent = label; const arrow = document.createElement('span'); arrow.className = 'fav-collapse-arrow'; arrow.textContent = favSectionCollapsed[type] ? '▶' : '▼'; hdr.appendChild(iconEl); hdr.appendChild(labelEl); hdr.appendChild(arrow); sec.appendChild(hdr); // ── Inhalt ──────────────────────────────────────── const row = document.createElement('div'); row.className = 'fav-chips-row'; row.style.cssText = favSectionCollapsed[type] ? 'display:none;' : 'display:flex;flex-wrap:wrap;gap:8px;'; const items = isFav ? favorites : recentRepos.slice(0, 8); items.forEach(entry => row.appendChild(makeChip(entry, isFav ? 'favorite' : 'recent', allRepos))); sec.appendChild(row); // ── Toggle-Logik ────────────────────────────────── hdr.addEventListener('click', () => { favSectionCollapsed[type] = !favSectionCollapsed[type]; const collapsed = favSectionCollapsed[type]; row.style.display = collapsed ? 'none' : 'flex'; arrow.textContent = collapsed ? '▶' : '▼'; // Zustand persistent speichern window.electronAPI.loadCredentials().then(c => { if (c && c.ok) { window.electronAPI.saveCredentials({ ...c, favCollapsedFavorites: favSectionCollapsed.favorites, favCollapsedRecent: favSectionCollapsed.recent }); } }).catch(() => {}); }); return sec; } function renderFavRecentSection(container, allRepos) { container.innerHTML = ''; const showFav = featureFavorites && favorites.length > 0; const showRec = featureRecent && recentRepos.length > 0; if (!showFav && !showRec) return; if (showFav) container.appendChild(makeFavSectionBlock('favorites', allRepos)); if (showRec) container.appendChild(makeFavSectionBlock('recent', allRepos)); // Trennlinie const div = document.createElement('div'); div.className = 'fav-divider'; container.appendChild(div); } function makeChip(entry, type, allRepos) { const isFav = type === 'favorite'; const chip = document.createElement('div'); chip.className = `fav-chip${isFav ? ' fav-chip--star' : ''}`; chip.title = `${entry.owner}/${entry.repo}`; const icon = document.createElement('span'); icon.className = 'fav-chip-icon'; icon.textContent = isFav ? '⭐' : '🕐'; const label = document.createElement('span'); label.className = 'fav-chip-label'; label.textContent = `${entry.owner}/${entry.repo}`; chip.appendChild(icon); chip.appendChild(label); if (!isFav && entry.openedAt) { const time = document.createElement('span'); time.className = 'fav-chip-time'; time.textContent = formatRelDate(entry.openedAt); chip.appendChild(time); } chip.onclick = () => { addToRecent(entry.owner, entry.repo, entry.cloneUrl); loadRepoContents(entry.owner, entry.repo, ''); }; chip.oncontextmenu = (ev) => { ev.preventDefault(); ev.stopPropagation(); showChipContextMenu(ev, entry, type); }; // Drag-Reorder (nur für Favoriten) if (isFav) { chip.draggable = true; chip.dataset.owner = entry.owner; chip.dataset.repo = entry.repo; chip.addEventListener('dragstart', (ev) => { ev.dataTransfer.effectAllowed = 'move'; ev.dataTransfer.setData('text/fav-owner', entry.owner); ev.dataTransfer.setData('text/fav-repo', entry.repo); chip.classList.add('fav-chip--dragging'); }); chip.addEventListener('dragend', () => chip.classList.remove('fav-chip--dragging')); chip.addEventListener('dragover', (ev) => { ev.preventDefault(); ev.dataTransfer.dropEffect = 'move'; chip.classList.add('fav-chip--drop-target'); }); chip.addEventListener('dragleave', () => chip.classList.remove('fav-chip--drop-target')); chip.addEventListener('drop', async (ev) => { ev.preventDefault(); chip.classList.remove('fav-chip--drop-target'); const srcOwner = ev.dataTransfer.getData('text/fav-owner'); const srcRepo = ev.dataTransfer.getData('text/fav-repo'); if (srcOwner === entry.owner && srcRepo === entry.repo) return; const fromIdx = favorites.findIndex(f => f.owner === srcOwner && f.repo === srcRepo); const toIdx = favorites.findIndex(f => f.owner === entry.owner && f.repo === entry.repo); if (fromIdx < 0 || toIdx < 0) return; // Reorder const [moved] = favorites.splice(fromIdx, 1); favorites.splice(toIdx, 0, moved); await window.electronAPI.saveFavorites(favorites); // Sektion neu rendern const sec = $('favRecentSection'); if (sec) { // allRepos fehlt hier, daher einfach neu laden const favBlock = sec.querySelector('.fav-chips-row'); if (favBlock) { const allChips = Array.from(favBlock.querySelectorAll('.fav-chip')); const movedChip = allChips.find(c => c.dataset.owner === srcOwner && c.dataset.repo === srcRepo); const targetChip = allChips.find(c => c.dataset.owner === entry.owner && c.dataset.repo === entry.repo); if (movedChip && targetChip) { favBlock.insertBefore(movedChip, fromIdx > toIdx ? targetChip : targetChip.nextSibling); } } } }); } return chip; } function showChipContextMenu(ev, entry, type) { const old = $('ctxMenu'); if (old) old.remove(); const menu = document.createElement('div'); menu.id = 'ctxMenu'; menu.className = 'context-menu'; menu.style.left = Math.min(ev.clientX, window.innerWidth - 240) + 'px'; menu.style.top = Math.min(ev.clientY, window.innerHeight - 160) + 'px'; const addItem = (icon, text, cb, color) => { const el = document.createElement('div'); el.className = 'context-item'; el.innerHTML = `${icon} ${text}`; if (color) el.style.color = color; el.onclick = () => { menu.remove(); cb(); }; menu.appendChild(el); }; addItem('📂', 'Öffnen', () => { addToRecent(entry.owner, entry.repo, entry.cloneUrl); loadRepoContents(entry.owner, entry.repo, ''); }); // Separator const sep = document.createElement('div'); sep.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;'; menu.appendChild(sep); if (type === 'favorite') { addItem('⭐', 'Aus Favoriten entfernen', async () => { await toggleFavorite(entry.owner, entry.repo, entry.cloneUrl); loadGiteaRepos(); }, '#f59e0b'); } else { addItem('⭐', 'Zu Favoriten hinzufügen', async () => { await toggleFavorite(entry.owner, entry.repo, entry.cloneUrl); loadGiteaRepos(); }); addItem('✕', 'Aus Verlauf entfernen', async () => { recentRepos = recentRepos.filter(r => !(r.owner === entry.owner && r.repo === entry.repo)); await window.electronAPI.saveRecent(recentRepos); loadGiteaRepos(); }, '#ef4444'); } document.body.appendChild(menu); setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10); } // Speichert den default_branch pro Repo (owner/repo -> 'main' oder 'master') let repoDefaultBranches = {}; function getDefaultBranch(owner, repo) { return repoDefaultBranches[`${owner}/${repo}`] || 'HEAD'; } // Navigations-Status für die Explorer-Ansicht let currentState = { view: 'none', // 'local', 'gitea-list', 'gitea-repo' owner: null, repo: null, path: '' }; // Clipboard für Cut & Paste let clipboard = { item: null, // { path, name, type, owner, repo, isGitea, isLocal, nodePath } action: null // 'cut' }; // Mehrfachauswahl let selectedItems = new Set(); // Set von item-Pfaden let isMultiSelectMode = false; // Zuletzt angeklicktes Item (für F2/Entf) let lastSelectedItem = null; // { type:'gitea', item, owner, repo } | { type:'local', node } // Feature-Flag für farbige Icons let featureColoredIcons = true; function setStatus(txt) { const s = $('status'); if (s) s.innerText = txt || ''; } /* ------------------------- TOAST NOTIFICATIONS ------------------------- */ function showToast(message, type = 'info', duration = 4000) { const container = (() => { let c = $('toastContainer'); if (!c) { c = document.createElement('div'); c.id = 'toastContainer'; c.style.cssText = ` position: fixed; bottom: 24px; right: 24px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; pointer-events: none; `; document.body.appendChild(c); } return c; })(); const colors = { error: { bg: 'rgba(239,68,68,0.15)', border: '#ef4444', icon: '✗' }, success: { bg: 'rgba(34,197,94,0.15)', border: '#22c55e', icon: '✓' }, info: { bg: 'rgba(0,212,255,0.12)', border: '#00d4ff', icon: 'ℹ' }, warning: { bg: 'rgba(245,158,11,0.15)', border: '#f59e0b', icon: '⚠' }, }; const c = colors[type] || colors.info; const toast = document.createElement('div'); toast.style.cssText = ` display: flex; align-items: flex-start; gap: 10px; padding: 12px 16px; background: ${c.bg}; border: 1px solid ${c.border}; border-left: 3px solid ${c.border}; border-radius: 10px; backdrop-filter: blur(12px); box-shadow: 0 4px 24px rgba(0,0,0,0.4); color: #fff; font-size: 13px; font-weight: 500; max-width: 360px; pointer-events: auto; cursor: pointer; opacity: 0; transform: translateX(20px); transition: opacity 220ms ease, transform 220ms ease; line-height: 1.4; `; const iconEl = document.createElement('span'); iconEl.style.cssText = `color: ${c.border}; font-weight: 700; font-size: 15px; flex-shrink: 0; margin-top: 1px;`; iconEl.textContent = c.icon; const msgEl = document.createElement('span'); msgEl.textContent = message; toast.appendChild(iconEl); toast.appendChild(msgEl); container.appendChild(toast); // Einblenden requestAnimationFrame(() => { toast.style.opacity = '1'; toast.style.transform = 'translateX(0)'; }); const dismiss = () => { toast.style.opacity = '0'; toast.style.transform = 'translateX(20px)'; setTimeout(() => toast.remove(), 220); }; toast.addEventListener('click', dismiss); setTimeout(dismiss, duration); } // Kurzformen function showError(msg) { setStatus(msg); showToast(msg, 'error'); } function showSuccess(msg) { setStatus(msg); showToast(msg, 'success', 3000); } function showWarning(msg) { setStatus(msg); showToast(msg, 'warning'); } // Löschen-Bestätigung als Toast (ersetzt confirm()) function showDeleteConfirm(message, onConfirm) { const container = (() => { let c = $('toastContainer'); if (!c) { c = document.createElement('div'); c.id = 'toastContainer'; c.style.cssText = 'position:fixed;bottom:24px;right:24px;z-index:99999;display:flex;flex-direction:column;gap:10px;pointer-events:none;'; document.body.appendChild(c); } return c; })(); const toast = document.createElement('div'); toast.style.cssText = ` padding: 14px 16px; background: rgba(239,68,68,0.15); border: 1px solid #ef4444; border-left: 3px solid #ef4444; border-radius: 10px; backdrop-filter: blur(12px); box-shadow: 0 4px 24px rgba(0,0,0,0.4); color: #fff; font-size: 13px; max-width: 360px; pointer-events: auto; opacity: 0; transform: translateX(20px); transition: opacity 220ms ease, transform 220ms ease; `; const msgEl = document.createElement('div'); msgEl.style.cssText = 'font-weight:600;margin-bottom:10px;'; msgEl.textContent = '🗑️ ' + message; const btns = document.createElement('div'); btns.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;'; const cancelBtn = document.createElement('button'); cancelBtn.textContent = 'Abbrechen'; cancelBtn.style.cssText = 'padding:5px 12px;border-radius:6px;border:1px solid rgba(255,255,255,0.2);background:transparent;color:#fff;cursor:pointer;font-size:12px;'; const confirmBtn = document.createElement('button'); confirmBtn.textContent = 'Löschen'; confirmBtn.style.cssText = 'padding:5px 12px;border-radius:6px;border:none;background:#ef4444;color:#fff;cursor:pointer;font-size:12px;font-weight:600;'; btns.appendChild(cancelBtn); btns.appendChild(confirmBtn); toast.appendChild(msgEl); toast.appendChild(btns); container.appendChild(toast); requestAnimationFrame(() => { toast.style.opacity = '1'; toast.style.transform = 'translateX(0)'; }); const dismiss = () => { toast.style.opacity = '0'; toast.style.transform = 'translateX(20px)'; setTimeout(() => toast.remove(), 220); }; cancelBtn.addEventListener('click', dismiss); confirmBtn.addEventListener('click', () => { dismiss(); onConfirm(); }); setTimeout(dismiss, 8000); } /* ------------------------- 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: getDefaultBranch(tab.owner, tab.repo) }); 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: getDefaultBranch(owner, repo) }); 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}`); showError('Fehler beim Laden der Datei'); } } catch (error) { console.error('Error opening Gitea file:', error); alert('Fehler beim Öffnen der Datei'); showError('Fehler'); } } // Farbige Icons pro Dateityp (emoji + Farb-Overlay via CSS-Klassen) const FILE_ICONS = { // Web js: { icon: '📄', color: '#f7df1e', label: 'JS' }, jsx: { icon: '📄', color: '#61dafb', label: 'JSX' }, ts: { icon: '📄', color: '#3178c6', label: 'TS' }, tsx: { icon: '📄', color: '#3178c6', label: 'TSX' }, html: { icon: '📄', color: '#e34c26', label: 'HTML' }, css: { icon: '📄', color: '#264de4', label: 'CSS' }, scss: { icon: '📄', color: '#cd6799', label: 'SCSS' }, vue: { icon: '📄', color: '#42b883', label: 'VUE' }, svelte:{ icon: '📄', color: '#ff3e00', label: 'SVE' }, // Backend py: { icon: '📄', color: '#3572a5', label: 'PY' }, java: { icon: '📄', color: '#b07219', label: 'JAVA' }, rb: { icon: '📄', color: '#701516', label: 'RB' }, php: { icon: '📄', color: '#4f5d95', label: 'PHP' }, go: { icon: '📄', color: '#00add8', label: 'GO' }, rs: { icon: '📄', color: '#dea584', label: 'RS' }, cs: { icon: '📄', color: '#178600', label: 'C#' }, cpp: { icon: '📄', color: '#f34b7d', label: 'C++' }, c: { icon: '📄', color: '#555555', label: 'C' }, // Config json: { icon: '📄', color: '#fbc02d', label: 'JSON' }, yaml: { icon: '📄', color: '#cb171e', label: 'YAML' }, yml: { icon: '📄', color: '#cb171e', label: 'YAML' }, toml: { icon: '📄', color: '#9c4221', label: 'TOML' }, env: { icon: '📄', color: '#ecd53f', label: 'ENV' }, xml: { icon: '📄', color: '#f60', label: 'XML' }, // Docs md: { icon: '📄', color: '#083fa1', label: 'MD' }, txt: { icon: '📄', color: '#888', label: 'TXT' }, pdf: { icon: '📄', color: '#e53935', label: 'PDF' }, // Shell sh: { icon: '📄', color: '#89e051', label: 'SH' }, bat: { icon: '📄', color: '#c1f12e', label: 'BAT' }, // Images png: { icon: '🖼️', color: '#4caf50', label: 'PNG' }, jpg: { icon: '🖼️', color: '#4caf50', label: 'JPG' }, jpeg: { icon: '🖼️', color: '#4caf50', label: 'JPG' }, gif: { icon: '🖼️', color: '#4caf50', label: 'GIF' }, svg: { icon: '🖼️', color: '#ff9800', label: 'SVG' }, webp: { icon: '🖼️', color: '#4caf50', label: 'WEBP' }, // Archives zip: { icon: '📦', color: '#ff9800', label: 'ZIP' }, tar: { icon: '📦', color: '#ff9800', label: 'TAR' }, gz: { icon: '📦', color: '#ff9800', label: 'GZ' }, }; function getFileIcon(fileName) { const ext = fileName.split('.').pop()?.toLowerCase(); const info = FILE_ICONS[ext]; if (!info) return '📄'; return info.icon; } // Gibt ein DOM-Element für das Explorer-Icon zurück (mit farbigem Badge wenn aktiviert) function makeFileIconEl(fileName, isDir = false) { const wrapper = document.createElement('div'); wrapper.className = 'item-icon'; if (isDir) { wrapper.textContent = '📁'; return wrapper; } const ext = fileName.split('.').pop()?.toLowerCase(); const info = featureColoredIcons ? FILE_ICONS[ext] : null; wrapper.textContent = info ? info.icon : '📄'; if (info) { const badge = document.createElement('span'); badge.className = 'file-type-badge'; badge.textContent = info.label; badge.style.background = info.color; // Helligkeit prüfen für Textfarbe const hex = info.color.replace('#',''); const r = parseInt(hex.slice(0,2)||'88',16); const g = parseInt(hex.slice(2,4)||'88',16); const b = parseInt(hex.slice(4,6)||'88',16); const lum = (0.299*r + 0.587*g + 0.114*b) / 255; badge.style.color = lum > 0.55 ? '#111' : '#fff'; wrapper.appendChild(badge); } return wrapper; } /* ------------------------- 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: getDefaultBranch(tab.owner, tab.repo) }); } 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) { showError('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'; } }); }); // ----------------------------------- // ── Favoriten & Zuletzt geöffnet ── // Sektion IMMER ins DOM einfügen (auch wenn leer), // damit der Stern-Klick später $('favRecentSection') findet if (featureFavorites || featureRecent) { const favSection = document.createElement('div'); favSection.id = 'favRecentSection'; favSection.style.cssText = 'grid-column: 1/-1;'; grid.appendChild(favSection); renderFavRecentSection(favSection, res.repos); } 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; // default_branch speichern (main ODER master je nach Repo) const defaultBranch = repo.default_branch || 'HEAD'; repoDefaultBranches[`${owner}/${repoName}`] = defaultBranch; const card = document.createElement('div'); card.className = 'item-card'; card.style.position = 'relative'; card.dataset.cloneUrl = cloneUrl; // Stern-Button (nur wenn Favoriten-Feature aktiv) if (featureFavorites) { const starBtn = document.createElement('button'); starBtn.className = 'fav-star-btn' + (isFavorite(owner, repoName) ? ' active' : ''); starBtn.title = isFavorite(owner, repoName) ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen'; starBtn.textContent = isFavorite(owner, repoName) ? '⭐' : '☆'; starBtn.addEventListener('click', async (e) => { e.stopPropagation(); await toggleFavorite(owner, repoName, cloneUrl); const nowFav = isFavorite(owner, repoName); starBtn.textContent = nowFav ? '⭐' : '☆'; starBtn.title = nowFav ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen'; starBtn.classList.toggle('active', nowFav); // Favoriten-Sektion live aktualisieren const sec = $('favRecentSection'); if (sec) renderFavRecentSection(sec, res.repos); }); card.appendChild(starBtn); } const iconEl = document.createElement('div'); iconEl.className = 'item-icon'; iconEl.textContent = '📦'; card.appendChild(iconEl); const nameEl = document.createElement('div'); nameEl.className = 'item-name'; nameEl.textContent = repoName; card.appendChild(nameEl); // Repo-Größe anzeigen if (repo.size != null) { const sizeEl = document.createElement('div'); sizeEl.className = 'repo-size-badge'; const kb = repo.size; sizeEl.textContent = kb >= 1024 ? `${(kb / 1024).toFixed(1)} MB` : `${kb} KB`; card.appendChild(sizeEl); } // --- 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 { showError('Download preparation failed'); } } catch (error) { console.error('Drag preparation error:', error); showError('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) { showWarning("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: getDefaultBranch(owner, repoName) }); if (!res.ok) { console.error("Upload Fehler:", res.error); showError("Fehler: " + res.error); } } catch (err) { console.error("Kritischer Upload Fehler:", err); setStatus("Upload fehlgeschlagen"); } } hideProgress(); setStatus('Upload abgeschlossen'); }); card.onclick = () => { addToRecent(owner, repoName, cloneUrl); 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); showError('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, getDefaultBranch(owner, repo)); } 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'}`); const ref = getDefaultBranch(owner, repo); try { const res = await window.electronAPI.getGiteaRepoContents({ owner, repo, path, ref }); if (!res.ok) { showError('Error: ' + (res.error || 'Unknown error')); return; } const grid = $('explorerGrid'); if (!grid) return; grid.innerHTML = ''; if (!res.items || res.items.length === 0) { const emptyMsg = res.empty ? '📭 Leeres Repository — noch keine Commits' : '📂 Leerer Ordner'; grid.innerHTML = `
${emptyMsg}
`; setStatus(res.empty ? 'Leeres Repository' : 'Leerer Ordner'); return; } res.items.forEach(item => { const card = document.createElement('div'); card.className = 'item-card'; // Farbiges Icon-Element const iconEl = makeFileIconEl(item.name, item.type === 'dir'); const nameEl = document.createElement('div'); nameEl.className = 'item-name'; nameEl.textContent = item.name; card.appendChild(iconEl); card.appendChild(nameEl); // lastSelectedItem tracken card.addEventListener('click', () => { lastSelectedItem = { type: 'gitea', item, owner, repo }; }); // 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: getDefaultBranch(owner, repo) }); } catch (error) { console.error('Upload error:', error); } } hideProgress(); loadRepoContents(owner, repo, path); }); if (item.type === 'dir') { card.onclick = (e) => { if (e.ctrlKey || e.metaKey) { if (selectedItems.has(item.path)) { selectedItems.delete(item.path); card.classList.remove('selected'); } else { selectedItems.add(item.path); card.classList.add('selected'); } return; } selectedItems.clear(); document.querySelectorAll('.item-card.selected').forEach(c => c.classList.remove('selected')); loadRepoContents(owner, repo, item.path); }; } else { card.onclick = (e) => { if (e.ctrlKey || e.metaKey) { if (selectedItems.has(item.path)) { selectedItems.delete(item.path); card.classList.remove('selected'); } else { selectedItems.add(item.path); card.classList.add('selected'); } return; } selectedItems.clear(); document.querySelectorAll('.item-card.selected').forEach(c => c.classList.remove('selected')); 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); showError('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); showError('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) { showError('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.dataset.path = node.path; // Farbiges Icon + Name const nodeIconEl = makeFileIconEl(node.name, node.isDirectory); const nodeNameEl = document.createElement('div'); nodeNameEl.className = 'item-name'; nodeNameEl.textContent = node.name; card.appendChild(nodeIconEl); card.appendChild(nodeNameEl); // lastSelectedItem tracken card.addEventListener('click', () => { lastSelectedItem = { type: 'local', node }; }); card.onclick = async (e) => { if (e.ctrlKey || e.metaKey) { // Mehrfachauswahl if (selectedItems.has(node.path)) { selectedItems.delete(node.path); card.classList.remove('selected'); } else { selectedItems.add(node.path); card.classList.add('selected'); } return; } // Normale Auswahl selectedItems.clear(); grid.querySelectorAll('.item-card.selected').forEach(c => c.classList.remove('selected')); if (!node.isDirectory) { openFileEditor(node.path, node.name); } }; card.oncontextmenu = (ev) => showLocalItemContextMenu(ev, node); grid.appendChild(card); }); } catch (error) { console.error('Error refreshing tree:', error); showError('Error loading file tree'); } } /* ------------------------- GIT ACTIONS ------------------------- */ async function pushLocalFolder() { if (!selectedFolder) { alert('Select local folder first'); return; } // Commit-Nachricht abfragen const message = await showCommitMessageModal(); if (message === null) return; // Abgebrochen 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, commitMessage: message }); if (res.ok) { setStatus('Push succeeded'); } else { showError('Push failed: ' + (res.error || 'Unknown error')); } } catch (error) { console.error('Push error:', error); showError('Push failed'); } finally { hideProgress(); } } // Modal für Commit-Nachricht function showCommitMessageModal() { return new Promise((resolve) => { const modal = document.createElement('div'); modal.className = 'modal'; modal.style.zIndex = '99999'; modal.innerHTML = `

💬 Commit-Nachricht

${['🐛 Fix Bug', '✨ Neues Feature', '📝 Dokumentation', '♻️ Refactoring', '🚀 Release'].map(t => `` ).join('')}
`; document.body.appendChild(modal); const input = modal.querySelector('#commitMsgInput'); input.focus(); // Quick-Buttons modal.querySelectorAll('.commit-quick-btn').forEach(btn => { btn.onclick = () => { input.value = btn.textContent.trim(); input.focus(); }; }); modal.querySelector('#btnCommitOk').onclick = () => { const val = input.value.trim() || 'Update via Git Manager GUI'; modal.remove(); resolve(val); }; modal.querySelector('#btnCommitCancel').onclick = () => { modal.remove(); resolve(null); }; input.addEventListener('keydown', (e) => { if (e.key === 'Enter') modal.querySelector('#btnCommitOk').click(); if (e.key === 'Escape') modal.querySelector('#btnCommitCancel').click(); }); }); } 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; }; // ── Favorit ── if (featureFavorites) { const isFav = isFavorite(owner, repoName); const favItem = createMenuItem( isFav ? '⭐' : '☆', isFav ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen', async () => { menu.remove(); await toggleFavorite(owner, repoName, cloneUrl); loadGiteaRepos(); }, isFav ? '#f59e0b' : null ); menu.appendChild(favItem); const sep = document.createElement('div'); sep.style.cssText = 'height:1px;background:rgba(255,255,255,0.08);margin:4px 0;'; menu.appendChild(sep); } 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: getDefaultBranch(owner, repoName) }); hideProgress(); setStatus('Upload complete'); } } catch (error) { console.error('Upload error:', error); hideProgress(); showError('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(); showSuccess('Repository deleted'); } else { showError('Delete failed: ' + res.error); } } catch (error) { console.error('Delete error:', error); showError('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'; // Menü positionieren (nicht außerhalb des Fensters) const menuW = 220, menuH = 360; const x = Math.min(ev.clientX, window.innerWidth - menuW); const y = Math.min(ev.clientY, window.innerHeight - menuH); menu.style.left = x + 'px'; menu.style.top = y + 'px'; const addSep = () => { const s = document.createElement('div'); s.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;'; menu.appendChild(s); }; const addItem = (icon, text, onClick, color = null) => { const el = document.createElement('div'); el.className = 'context-item'; el.innerHTML = `${icon} ${text}`; if (color) el.style.color = color; el.onclick = () => { menu.remove(); onClick(); }; menu.appendChild(el); }; // Mehrfachauswahl-Info if (selectedItems.size > 1 && selectedItems.has(item.path)) { const infoEl = document.createElement('div'); infoEl.style.cssText = 'padding:8px 14px;font-size:11px;color:var(--accent-primary);font-weight:600;'; infoEl.textContent = `${selectedItems.size} Elemente ausgewählt`; menu.appendChild(infoEl); addSep(); addItem('🗑️', `Alle ${selectedItems.size} löschen`, async () => { if (!confirm(`${selectedItems.size} Elemente wirklich löschen?`)) return; showProgress(0, 'Lösche...'); let done = 0; for (const p of selectedItems) { await window.electronAPI.deleteFile({ path: p, owner, repo, isGitea: true }); done++; showProgress(Math.round((done / selectedItems.size) * 100), `Lösche ${done}/${selectedItems.size}`); } selectedItems.clear(); hideProgress(); setStatus('Bulk-Delete abgeschlossen'); loadRepoContents(owner, repo, currentState.path); }, '#ef4444'); document.body.appendChild(menu); setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10); return; } // --- ÖFFNEN --- addItem( item.type === 'dir' ? '📂' : '✏️', item.type === 'dir' ? 'Öffnen' : 'Im Editor öffnen', () => { if (item.type === 'dir') loadRepoContents(owner, repo, item.path); else openGiteaFileInEditor(owner, repo, item.path, item.name); } ); addSep(); // --- NEU ERSTELLEN (immer sichtbar) --- addItem('📄', 'Neue Datei erstellen', () => showNewGiteaItemModal(owner, repo, item.type === 'dir' ? item.path : currentState.path, 'file')); addItem('📁', 'Neuen Ordner erstellen', () => showNewGiteaItemModal(owner, repo, item.type === 'dir' ? item.path : currentState.path, 'folder')); addSep(); // --- UMBENENNEN --- addItem('✏️', 'Umbenennen', () => showGiteaRenameModal(item, owner, repo)); // --- CUT & PASTE --- addItem('✂️', 'Ausschneiden (Cut)', () => { clipboard = { item: { ...item, owner, repo, isGitea: true }, action: 'cut' }; setStatus(`✂️ "${item.name}" ausgeschnitten — Zielordner öffnen und Einfügen wählen`); }); if (clipboard.item && clipboard.item.isGitea && item.type === 'dir') { addItem('📋', `Einfügen: "${clipboard.item.name}"`, async () => { await pasteGiteaItem(owner, repo, item.path); }); } addSep(); // --- DOWNLOAD --- if (item.type === 'file') { addItem('📥', 'Herunterladen', async () => { const res = await window.electronAPI.downloadGiteaFile({ owner, repo, path: item.path }); setStatus(res.ok ? `Gespeichert: ${res.savedTo}` : 'Download fehlgeschlagen'); }); } else { addItem('📥', 'Ordner herunterladen', async () => { showProgress(0, `Lade ${item.name}...`); const res = await window.electronAPI.downloadGiteaFolder({ owner, repo, path: item.path }); hideProgress(); setStatus(res.ok ? `Gespeichert: ${res.savedTo}` : 'Download fehlgeschlagen'); }); } addSep(); // --- LÖSCHEN --- addItem('🗑️', 'Löschen', async () => { if (!confirm(`"${item.name}" wirklich löschen?`)) return; showProgress(0, `Lösche ${item.name}...`); const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: true }); hideProgress(); if (res && res.ok) { setStatus(`${item.name} gelöscht`); loadRepoContents(owner, repo, currentState.path); } else { showError('Löschen fehlgeschlagen: ' + (res?.error || '')); alert('Löschen fehlgeschlagen:\n' + (res?.error || 'Unbekannter Fehler')); } }, '#ef4444'); document.body.appendChild(menu); setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10); } /* ------------------------- LOKALES KONTEXT-MENÜ ------------------------- */ function showLocalItemContextMenu(ev, node) { ev.preventDefault(); ev.stopPropagation(); const old = $('ctxMenu'); if (old) old.remove(); const menu = document.createElement('div'); menu.id = 'ctxMenu'; menu.className = 'context-menu'; const menuW = 220, menuH = 360; const x = Math.min(ev.clientX, window.innerWidth - menuW); const y = Math.min(ev.clientY, window.innerHeight - menuH); menu.style.left = x + 'px'; menu.style.top = y + 'px'; const addSep = () => { const s = document.createElement('div'); s.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;'; menu.appendChild(s); }; const addItem = (icon, text, onClick, color = null) => { const el = document.createElement('div'); el.className = 'context-item'; el.innerHTML = `${icon} ${text}`; if (color) el.style.color = color; el.onclick = () => { menu.remove(); onClick(); }; menu.appendChild(el); }; // Mehrfachauswahl-Bulk-Delete if (selectedItems.size > 1 && selectedItems.has(node.path)) { const infoEl = document.createElement('div'); infoEl.style.cssText = 'padding:8px 14px;font-size:11px;color:var(--accent-primary);font-weight:600;'; infoEl.textContent = `${selectedItems.size} Elemente ausgewählt`; menu.appendChild(infoEl); addSep(); addItem('🗑️', `Alle ${selectedItems.size} löschen`, async () => { if (!confirm(`${selectedItems.size} Elemente wirklich löschen?`)) return; for (const p of selectedItems) { await window.electronAPI.deleteFile({ path: p }); } selectedItems.clear(); setStatus('Bulk-Delete abgeschlossen'); if (selectedFolder) refreshLocalTree(selectedFolder); }, '#ef4444'); document.body.appendChild(menu); setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10); return; } // --- ÖFFNEN --- if (!node.isDirectory) { addItem('✏️', 'Im Editor öffnen', () => openFileEditor(node.path, node.name)); addSep(); } // --- NEU ERSTELLEN --- const targetDir = node.isDirectory ? node.path : require('path').dirname(node.path); addItem('📄', 'Neue Datei erstellen', () => showNewLocalItemModal(targetDir, 'file')); addItem('📁', 'Neuen Ordner erstellen', () => showNewLocalItemModal(targetDir, 'folder')); addSep(); // --- UMBENENNEN --- addItem('✏️', 'Umbenennen', () => showLocalRenameModal(node)); // --- CUT & PASTE --- addItem('✂️', 'Ausschneiden (Cut)', () => { clipboard = { item: { ...node, isLocal: true }, action: 'cut' }; setStatus(`✂️ "${node.name}" ausgeschnitten`); }); if (clipboard.item && clipboard.item.isLocal && node.isDirectory) { addItem('📋', `Einfügen: "${clipboard.item.name}"`, async () => { await pasteLocalItem(node.path); }); } addSep(); // --- DOWNLOAD / KOPIEREN --- addItem('📥', 'Kopieren nach...', async () => { const destFolder = await window.electronAPI.selectFolder(); if (!destFolder) return; const res = await window.electronAPI.copyLocalItem({ src: node.path, destDir: destFolder }); setStatus(res?.ok ? `Kopiert nach: ${destFolder}` : 'Kopieren fehlgeschlagen: ' + (res?.error || '')); }); addSep(); // --- LÖSCHEN --- addItem('🗑️', 'Löschen', async () => { if (!confirm(`"${node.name}" wirklich löschen?`)) return; const res = await window.electronAPI.deleteFile({ path: node.path }); if (res && res.ok) { setStatus(`${node.name} gelöscht`); if (selectedFolder) refreshLocalTree(selectedFolder); } else { showError('Löschen fehlgeschlagen: ' + (res?.error || '')); } }, '#ef4444'); document.body.appendChild(menu); setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10); } /* ========================================= MODAL HELPER FUNKTIONEN ========================================= */ // Gitea: Umbenennen function showGiteaRenameModal(item, owner, repo) { showInputModal({ title: `✏️ Umbenennen`, label: 'Neuer Name', defaultValue: item.name, confirmText: 'Umbenennen', onConfirm: async (newName) => { if (!newName || newName === item.name) return; setStatus('Umbenennen...'); const parentPath = item.path.split('/').slice(0, -1).join('/'); const newPath = parentPath ? `${parentPath}/${newName}` : newName; const res = await window.electronAPI.renameGiteaItem({ owner, repo, oldPath: item.path, newPath, isDir: item.type === 'dir' }); if (res?.ok) { setStatus(`Umbenannt in "${newName}"`); loadRepoContents(owner, repo, currentState.path); } else { alert('Umbenennen fehlgeschlagen:\n' + (res?.error || 'Unbekannter Fehler')); showError('Fehler beim Umbenennen'); } } }); } // Gitea: Neue Datei / Ordner function showNewGiteaItemModal(owner, repo, parentPath, type) { showInputModal({ title: type === 'file' ? '📄 Neue Datei' : '📁 Neuer Ordner', label: type === 'file' ? 'Dateiname (z.B. README.md)' : 'Ordnername', defaultValue: '', confirmText: 'Erstellen', onConfirm: async (name) => { if (!name) return; const targetPath = parentPath ? `${parentPath}/${name}` : name; const res = await window.electronAPI.createGiteaItem({ owner, repo, path: targetPath, type }); if (res?.ok) { setStatus(`"${name}" erstellt`); loadRepoContents(owner, repo, currentState.path); } else { alert('Erstellen fehlgeschlagen:\n' + (res?.error || '')); } } }); } // Lokal: Umbenennen function showLocalRenameModal(node) { showInputModal({ title: `✏️ Umbenennen`, label: 'Neuer Name', defaultValue: node.name, confirmText: 'Umbenennen', onConfirm: async (newName) => { if (!newName || newName === node.name) return; const res = await window.electronAPI.renameLocalItem({ oldPath: node.path, newName }); if (res?.ok) { setStatus(`Umbenannt in "${newName}"`); if (selectedFolder) refreshLocalTree(selectedFolder); } else { alert('Umbenennen fehlgeschlagen:\n' + (res?.error || '')); } } }); } // Lokal: Neue Datei / Ordner function showNewLocalItemModal(parentDir, type) { showInputModal({ title: type === 'file' ? '📄 Neue Datei' : '📁 Neuer Ordner', label: type === 'file' ? 'Dateiname (z.B. README.md)' : 'Ordnername', defaultValue: '', confirmText: 'Erstellen', onConfirm: async (name) => { if (!name) return; const res = await window.electronAPI.createLocalItem({ parentDir, name, type }); if (res?.ok) { setStatus(`"${name}" erstellt`); if (selectedFolder) refreshLocalTree(selectedFolder); } else { alert('Erstellen fehlgeschlagen:\n' + (res?.error || '')); } } }); } // Gitea: Einfügen nach Cut async function pasteGiteaItem(owner, repo, destFolderPath) { if (!clipboard.item || !clipboard.item.isGitea) return; const src = clipboard.item; const newPath = destFolderPath ? `${destFolderPath}/${src.name}` : src.name; setStatus(`Verschiebe "${src.name}"...`); showProgress(0, `Verschiebe...`); const res = await window.electronAPI.renameGiteaItem({ owner, repo, oldPath: src.path, newPath, isDir: src.type === 'dir' }); hideProgress(); if (res?.ok) { clipboard = { item: null, action: null }; setStatus(`"${src.name}" verschoben`); loadRepoContents(owner, repo, currentState.path); } else { alert('Verschieben fehlgeschlagen:\n' + (res?.error || '')); showError('Fehler beim Verschieben'); } } // Lokal: Einfügen nach Cut async function pasteLocalItem(destDir) { if (!clipboard.item || !clipboard.item.isLocal) return; const src = clipboard.item; setStatus(`Verschiebe "${src.name}"...`); const res = await window.electronAPI.moveLocalItem({ srcPath: src.path, destDir }); if (res?.ok) { clipboard = { item: null, action: null }; setStatus(`"${src.name}" verschoben`); if (selectedFolder) refreshLocalTree(selectedFolder); } else { alert('Verschieben fehlgeschlagen:\n' + (res?.error || '')); } } // Generic Input Modal function showInputModal({ title, label, defaultValue, confirmText, onConfirm }) { const modal = document.createElement('div'); modal.className = 'modal'; modal.style.zIndex = '99999'; modal.innerHTML = `

${title}

`; document.body.appendChild(modal); const input = modal.querySelector('#inputModalField'); input.focus(); input.select(); modal.querySelector('#inputModalOk').onclick = () => { const val = input.value.trim(); modal.remove(); onConfirm(val); }; modal.querySelector('#inputModalCancel').onclick = () => modal.remove(); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') modal.querySelector('#inputModalOk').click(); if (e.key === 'Escape') modal.querySelector('#inputModalCancel').click(); }); modal.onclick = (e) => { if (e.target === modal) modal.remove(); }; } /* ------------------------- 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: getDefaultBranch(owner, repo) }); if (res.ok) { console.log("Content of", filePath, ":", res.content); setStatus(`Previewed: ${filePath}`); } else { showError('Preview failed'); } } catch (error) { console.error('Preview error:', error); showError('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'); showSuccess('Repository created'); loadGiteaRepos(); } else { showError('Create failed: ' + (res.error || 'Unknown error')); } } catch (error) { console.error('Create repo error:', error); showError('Create failed'); } } /* ------------------------- GLOBALER DROP-HANDLER FÜR REPO-ANSICHT ------------------------- */ /* ------------------------- HINTERGRUND KONTEXT-MENÜ (Rechtsklick auf leere Fläche) ------------------------- */ function setupBackgroundContextMenu() { // Listener auf #main statt explorerGrid, da Grid kleiner als sichtbarer Bereich sein kann const mainEl = $('main'); if (!mainEl) return; mainEl.addEventListener('contextmenu', (ev) => { // Nicht auslösen wenn auf eine Karte oder interaktives Element geklickt wird if (ev.target.closest('.item-card, .release-card, .commit-card, .fav-chip, .fav-star-btn, button, input, textarea, select, a')) return; // Nur in Repo- oder Lokal-Ansicht if (currentState.view !== 'gitea-repo' && currentState.view !== 'local') return; ev.preventDefault(); ev.stopPropagation(); const old = $('ctxMenu'); if (old) old.remove(); const menu = document.createElement('div'); menu.id = 'ctxMenu'; menu.className = 'context-menu'; const menuW = 220, menuH = 160; const x = Math.min(ev.clientX, window.innerWidth - menuW); const y = Math.min(ev.clientY, window.innerHeight - menuH); menu.style.left = x + 'px'; menu.style.top = y + 'px'; // Aktuelle Pfad-Info als Header const header = document.createElement('div'); header.style.cssText = 'padding:8px 14px 4px;font-size:11px;color:var(--text-muted);letter-spacing:0.5px;'; const currentPath = currentState.view === 'gitea-repo' ? (currentState.path || 'Root') : (selectedFolder ? selectedFolder.split(/[\\/]/).pop() : 'Lokal'); header.textContent = `📂 ${currentPath}`; menu.appendChild(header); const sep = document.createElement('div'); sep.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;'; menu.appendChild(sep); const addItem = (icon, text, onClick) => { const el = document.createElement('div'); el.className = 'context-item'; el.innerHTML = `${icon} ${text}`; el.onclick = () => { menu.remove(); onClick(); }; menu.appendChild(el); }; if (currentState.view === 'gitea-repo') { const { owner, repo, path: currentPath } = currentState; addItem('📄', 'Neue Datei erstellen', () => showNewGiteaItemModal(owner, repo, currentPath, 'file') ); addItem('📁', 'Neuen Ordner erstellen', () => showNewGiteaItemModal(owner, repo, currentPath, 'folder') ); // Einfügen: gleiche Quelle ODER Cross-Paste von Lokal if (clipboard.item) { const sep2 = document.createElement('div'); sep2.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;'; menu.appendChild(sep2); if (clipboard.item.isGitea) { addItem('📋', `Einfügen: "${clipboard.item.name}"`, () => pasteGiteaItem(owner, repo, currentPath) ); } else if (clipboard.item.isLocal) { addItem('📋', `⬆️ Von Lokal einfügen: "${clipboard.item.name}"`, async () => { showProgress(0, `Lade "${clipboard.item.name}" hoch...`); try { await window.electronAPI.uploadAndPush({ localFolder: clipboard.item.path, owner, repo, destPath: currentPath, branch: getDefaultBranch(owner, repo) }); showSuccess(`"${clipboard.item.name}" nach Gitea kopiert`); loadRepoContents(owner, repo, currentState.path); } catch(e) { showError('Cross-Paste fehlgeschlagen'); } finally { hideProgress(); } }); } } } else if (currentState.view === 'local' && selectedFolder) { addItem('📄', 'Neue Datei erstellen', () => showNewLocalItemModal(selectedFolder, 'file') ); addItem('📁', 'Neuen Ordner erstellen', () => showNewLocalItemModal(selectedFolder, 'folder') ); if (clipboard.item) { const sep2 = document.createElement('div'); sep2.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;'; menu.appendChild(sep2); if (clipboard.item.isLocal) { addItem('📋', `Einfügen: "${clipboard.item.name}"`, () => pasteLocalItem(selectedFolder) ); } else if (clipboard.item.isGitea) { addItem('📋', `⬇️ Von Gitea einfügen: "${clipboard.item.name}"`, async () => { showProgress(0, `Lade "${clipboard.item.name}" herunter...`); try { await window.electronAPI.downloadGiteaFolder({ owner: clipboard.item.owner, repo: clipboard.item.repo, giteaPath: clipboard.item.path, localPath: selectedFolder }); showSuccess(`"${clipboard.item.name}" nach Lokal kopiert`); refreshLocalTree(selectedFolder); } catch(e) { showError('Cross-Paste fehlgeschlagen'); } finally { hideProgress(); } }); } } } document.body.appendChild(menu); setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10); }); } 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) { showWarning("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: getDefaultBranch(owner, repo) }); if (!res.ok) { console.error("Upload error:", res.error); showError("Error: " + res.error); } else { setStatus(`Uploaded: ${baseName}`); } } catch (err) { console.error("Critical upload error:", err); showError("Upload failed"); } } hideProgress(); // Refresh current view setTimeout(() => { loadRepoContents(owner, repo, targetPath); }, 1000); }); } /* ------------------------- INITIALISIERUNG ------------------------- */ window.addEventListener('DOMContentLoaded', async () => { // Favoriten & Verlauf vorladen await loadFavoritesAndRecent(); // 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 and auto-login if available try { const creds = await window.electronAPI.loadCredentials(); if (creds) { // Fülle Settings-Felder if ($('githubToken')) $('githubToken').value = creds.githubToken || ''; if ($('giteaToken')) $('giteaToken').value = creds.giteaToken || ''; if ($('giteaURL')) $('giteaURL').value = creds.giteaURL || ''; // Feature-Flags aus gespeicherten Einstellungen if (typeof creds.featureFavorites === 'boolean') featureFavorites = creds.featureFavorites; if (typeof creds.featureRecent === 'boolean') featureRecent = creds.featureRecent; if (typeof creds.compactMode === 'boolean') compactMode = creds.compactMode; if (typeof creds.featureColoredIcons === 'boolean') featureColoredIcons = creds.featureColoredIcons; document.body.classList.toggle('compact-mode', compactMode); // Collapse-Zustand wiederherstellen if (typeof creds.favCollapsedFavorites === 'boolean') favSectionCollapsed.favorites = creds.favCollapsedFavorites; if (typeof creds.favCollapsedRecent === 'boolean') favSectionCollapsed.recent = creds.favCollapsedRecent; // Settings-Checkboxen befüllen const cbFav = $('settingFavorites'); const cbRec = $('settingRecent'); const cbCompact = $('settingCompact'); if (cbFav) cbFav.checked = featureFavorites; if (cbRec) cbRec.checked = featureRecent; if (cbCompact) cbCompact.checked = compactMode; const cbColorIcons = $('settingColoredIcons'); if (cbColorIcons) cbColorIcons.checked = featureColoredIcons; // 🆕 AUTO-LOGIN: Wenn Gitea-Credentials vorhanden sind, lade sofort die Repos if (creds.giteaToken && creds.giteaURL) { console.log('✅ Credentials gefunden - Auto-Login wird gestartet...'); setStatus('Lade deine Projekte...'); // Kurze Verzögerung damit UI fertig geladen ist setTimeout(() => { loadGiteaRepos(); }, 500); } else { console.log('ℹ️ Keine vollständigen Gitea-Credentials - bitte in Settings eintragen'); setStatus('Bereit - bitte Settings konfigurieren'); } } else { console.log('ℹ️ Keine Credentials gespeichert'); setStatus('Bereit - bitte Settings konfigurieren'); } } catch (error) { console.error('Error loading credentials:', error); showError('Fehler beim Laden der Einstellungen'); } // Rest of Event Handlers... (bleibt unverändert) // 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 { // Feature-Flags aus Checkboxen lesen const cbFav = $('settingFavorites'); const cbRec = $('settingRecent'); const cbCompact = $('settingCompact'); featureFavorites = cbFav ? cbFav.checked : true; featureRecent = cbRec ? cbRec.checked : true; compactMode = cbCompact ? cbCompact.checked : false; const cbColorIcons2 = $('settingColoredIcons'); featureColoredIcons = cbColorIcons2 ? cbColorIcons2.checked : true; document.body.classList.toggle('compact-mode', compactMode); const data = { githubToken: $('githubToken').value, giteaToken: $('giteaToken').value, giteaURL: $('giteaURL').value, featureFavorites, featureRecent, compactMode, featureColoredIcons, favCollapsedFavorites: favSectionCollapsed.favorites, favCollapsedRecent: favSectionCollapsed.recent }; await window.electronAPI.saveCredentials(data); $('settingsModal').classList.add('hidden'); showSuccess('Settings saved'); // Ansicht aktualisieren falls Feature-Flags geändert loadGiteaRepos(); } catch (error) { console.error('Error saving settings:', error); showError('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(); } }; } // 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'); } } // F2 - Umbenennen if (e.key === 'F2' && lastSelectedItem && !currentActiveTab) { e.preventDefault(); if (lastSelectedItem.type === 'gitea') { showGiteaRenameModal(lastSelectedItem.item, lastSelectedItem.owner, lastSelectedItem.repo); } else if (lastSelectedItem.type === 'local') { showLocalRenameModal(lastSelectedItem.node); } } // Entf - Löschen mit Bestätigungs-Toast if (e.key === 'Delete' && lastSelectedItem && !currentActiveTab) { e.preventDefault(); if (lastSelectedItem.type === 'gitea') { const { item, owner, repo } = lastSelectedItem; showDeleteConfirm(`"${item.name}" wirklich löschen?`, async () => { const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: true }); if (res?.ok) { showSuccess(`"${item.name}" gelöscht`); loadRepoContents(owner, repo, currentState.path); lastSelectedItem = null; } else showError('Löschen fehlgeschlagen: ' + (res?.error || '')); }); } else if (lastSelectedItem.type === 'local') { const { node } = lastSelectedItem; showDeleteConfirm(`"${node.name}" wirklich löschen?`, async () => { const res = await window.electronAPI.deleteFile({ path: node.path }); if (res?.ok) { showSuccess(`"${node.name}" gelöscht`); if (selectedFolder) refreshLocalTree(selectedFolder); lastSelectedItem = null; } else showError('Löschen fehlgeschlagen: ' + (res?.error || '')); }); } } }); // 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(); setupBackgroundContextMenu(); 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) { showError('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); showError('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 (MIT DATEI-UPLOAD) ------------------------- */ function showCreateReleaseModal(owner, repo) { const modal = document.createElement('div'); modal.className = 'modal'; modal.innerHTML = `

🚀 Neues Release erstellen

`; document.body.appendChild(modal); // Variable zum Speichern des gewählten Dateipfads let selectedAssetPath = null; // Event Listener: Datei auswählen $('btnSelectReleaseAsset').onclick = async () => { try { const res = await window.electronAPI.selectFile(); if (res.ok && res.files && res.files.length > 0) { selectedAssetPath = res.files[0]; const fileName = selectedAssetPath.split(/[\\/]/).pop(); $('releaseAssetInput').value = fileName; $('releaseAssetInput').style.color = 'var(--text-primary)'; $('releaseAssetInput').style.borderColor = 'var(--accent-primary)'; } } catch (error) { console.error('Fehler beim Auswählen der Datei:', error); alert('Konnte Dateidialog nicht öffnen.'); } }; // Event Listener: Release erstellen $('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; } setStatus('Creating release...'); try { // 1. Release erstellen const res = await window.electronAPI.createRelease({ owner, repo, tag_name: tag, name, body, target_commitish: target, prerelease, draft }); if (res.ok) { // 2. Falls eine Datei ausgewählt wurde, direkt hochladen if (selectedAssetPath) { setStatus('Release erstellt. Lade Datei hoch...'); showProgress(50, 'Uploading Asset...'); try { const fileName = selectedAssetPath.split(/[\\/]/).pop(); const uploadRes = await window.electronAPI.uploadReleaseAsset({ owner, repo, releaseId: res.release.id, filePath: selectedAssetPath, fileName }); if (uploadRes.ok) { setStatus(`Release "${tag}" und Asset erstellt!`); } else { console.error('Asset Upload fehlgeschlagen:', uploadRes.error); alert(`Release erstellt, aber Asset Upload fehlgeschlagen: ${uploadRes.error}`); showWarning('Release erstellt (Upload Fehler)'); } } catch (uploadErr) { console.error('Upload error:', uploadErr); alert('Release erstellt, aber Fehler beim Hochladen der Datei.'); showWarning('Release erstellt (Upload Fehler)'); } finally { hideProgress(); } } else { setStatus('Release created!'); } modal.remove(); loadRepoReleases(owner, repo); // Liste neu laden } else { showError('Failed: ' + res.error); alert('Fehler beim Erstellen des Releases: ' + res.error); } } catch (error) { console.error('Create release error:', error); showError('Create failed'); alert('Ein unerwarteter Fehler ist aufgetreten.'); } }; $('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 filePath = res.files[0]; const fileName = filePath.split(/[\\/]/).pop(); 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 { showError('Upload failed: ' + uploadRes.error); } } catch (error) { console.error('Upload asset error:', error); hideProgress(); showError('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: 'HEAD', 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) { showError('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); showError('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); });