// 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; let featureAutostart = false; let repoNameValidationTimer = null; let batchCloneValidationTimer = null; let activityHeatmapCollapsed = true; let activityHeatmapRangeMonths = 20; // Sidebar-State für die linke Favoriten/Verlauf-Fläche let sidebarMode = 'favorites'; let currentGiteaRepos = []; 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); refreshFavHistoryUi(); } 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); refreshFavHistoryUi(); } 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'); } function setPlatformSelection(platform) { const platformInput = $('platform'); if (platformInput) { platformInput.value = platform; } document.querySelectorAll('.platform-option').forEach(button => { const isActive = button.dataset.platform === platform; button.classList.toggle('active', isActive); button.setAttribute('aria-pressed', isActive ? 'true' : 'false'); }); } function initializePlatformSelection() { const platformInput = $('platform'); const initialPlatform = platformInput?.value || 'gitea'; setPlatformSelection(initialPlatform); document.querySelectorAll('.platform-option').forEach(button => { button.addEventListener('click', () => { setPlatformSelection(button.dataset.platform || 'gitea'); }); }); } /* 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 renderFavHistorySidebar(allRepos) { const sidebar = $('favHistorySidebar'); if (!sidebar) return; const hasFavFeature = featureFavorites; const hasRecFeature = featureRecent; const canShowSidebar = hasFavFeature || hasRecFeature; // Sidebar nur einblenden wenn Feature aktiv — nichts am main/explorerGrid ändern sidebar.classList.toggle('visible', canShowSidebar); if (!canShowSidebar) { sidebar.innerHTML = ''; return; } if (!hasFavFeature && sidebarMode === 'favorites') sidebarMode = 'recent'; if (!hasRecFeature && sidebarMode === 'recent') sidebarMode = 'favorites'; const activeType = sidebarMode; const items = activeType === 'favorites' ? favorites : recentRepos; const inner = document.createElement('div'); inner.className = 'fav-history-sidebar-inner'; const tabs = document.createElement('div'); tabs.className = 'fav-history-switch'; if (hasFavFeature) { const btnFav = document.createElement('button'); btnFav.className = 'fav-history-tab' + (activeType === 'favorites' ? ' active' : ''); btnFav.textContent = `⭐ Favoriten (${favorites.length})`; btnFav.onclick = () => { sidebarMode = 'favorites'; renderFavHistorySidebar(allRepos); }; tabs.appendChild(btnFav); } if (hasRecFeature) { const btnRec = document.createElement('button'); btnRec.className = 'fav-history-tab' + (activeType === 'recent' ? ' active' : ''); btnRec.textContent = `🕐 Verlauf (${recentRepos.length})`; btnRec.onclick = () => { sidebarMode = 'recent'; renderFavHistorySidebar(allRepos); }; tabs.appendChild(btnRec); } const list = document.createElement('div'); list.className = 'fav-history-list'; const visibleItems = activeType === 'favorites' ? items : items.slice(0, 30); if (visibleItems.length === 0) { const empty = document.createElement('div'); empty.className = 'fav-history-empty'; empty.textContent = activeType === 'favorites' ? 'Noch keine Favoriten markiert.' : 'Noch kein Verlauf vorhanden.'; list.appendChild(empty); } else { visibleItems.forEach((entry) => { const itemBtn = document.createElement('button'); itemBtn.className = 'fav-history-item'; itemBtn.type = 'button'; const name = document.createElement('span'); name.className = 'fav-history-item-name'; name.textContent = entry.repo || '-'; const meta = document.createElement('span'); meta.className = 'fav-history-item-meta'; if (activeType === 'recent' && entry.openedAt) { meta.textContent = `${entry.owner || '-'} • ${formatRelDate(entry.openedAt)}`; } else { meta.textContent = entry.owner || '-'; } itemBtn.appendChild(name); itemBtn.appendChild(meta); itemBtn.onclick = () => { addToRecent(entry.owner, entry.repo, entry.cloneUrl); loadRepoContents(entry.owner, entry.repo, ''); }; itemBtn.oncontextmenu = (ev) => { ev.preventDefault(); ev.stopPropagation(); showChipContextMenu(ev, entry, activeType === 'favorites' ? 'favorite' : 'recent'); }; list.appendChild(itemBtn); }); } inner.appendChild(tabs); inner.appendChild(list); sidebar.innerHTML = ''; sidebar.appendChild(inner); } function refreshFavHistoryUi() { renderFavHistorySidebar(currentGiteaRepos); // Stern-Buttons im Grid aktualisieren document.querySelectorAll('.fav-star-btn').forEach(btn => { const card = btn.closest('.item-card'); if (!card) return; const owner = card.dataset.owner; const repo = card.dataset.repo; if (!owner || !repo) return; const active = isFavorite(owner, repo); btn.classList.toggle('active', active); btn.textContent = active ? '⭐' : '☆'; btn.title = active ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen'; }); } 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: '' }; const MAX_ACTIVITY_ITEMS = 300; let activityEntries = []; let retryQueueCount = 0; const HEATMAP_CACHE_MS = 5 * 60 * 1000; let remoteHeatmapFetchState = 'idle'; // idle | ok | error let remoteHeatmapFetchedAt = 0; let remoteHeatmapCounts = new Map(); let remoteHeatmapUsername = ''; function logActivity(level, message) { const entry = { id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, level: level || 'info', message: String(message || ''), ts: new Date().toISOString() }; activityEntries.unshift(entry); if (activityEntries.length > MAX_ACTIVITY_ITEMS) { activityEntries = activityEntries.slice(0, MAX_ACTIVITY_ITEMS); } renderActivityLog(); refreshActivityHeatmapIfVisible(); } function refreshActivityHeatmapIfVisible() { const host = $('repoActivityHeatmapHost'); if (!host) return; renderActivityHeatmap(host); } function normalizeHeatmapEntryDate(value) { if (value == null) return null; if (typeof value === 'string') { const m = value.match(/^(\d{4}-\d{2}-\d{2})/); if (m) return m[1]; const d = new Date(value); if (!Number.isNaN(d.getTime())) return formatDateKey(d); return null; } if (typeof value === 'number' && Number.isFinite(value)) { const ms = value < 1e12 ? value * 1000 : value; const d = new Date(ms); if (Number.isNaN(d.getTime())) return null; return formatDateKey(d); } if (value instanceof Date && !Number.isNaN(value.getTime())) { return formatDateKey(value); } return null; } function setRemoteHeatmapEntries(entries) { const next = new Map(); if (Array.isArray(entries)) { for (const item of entries) { if (!item) continue; const date = normalizeHeatmapEntryDate(item.date || item.day || item.timestamp || item.ts); if (!date) continue; const countNum = Number(item.count ?? item.value ?? item.contributions ?? 0); const count = Number.isFinite(countNum) ? Math.max(0, Math.floor(countNum)) : 0; next.set(date, (next.get(date) || 0) + count); } } remoteHeatmapCounts = next; } async function loadRemoteHeatmapData(force = false) { const now = Date.now(); if (!force && remoteHeatmapFetchState === 'ok' && (now - remoteHeatmapFetchedAt) < HEATMAP_CACHE_MS) { return; } if (!window.electronAPI || typeof window.electronAPI.getGiteaUserHeatmap !== 'function') { remoteHeatmapFetchState = 'error'; return; } try { const res = await window.electronAPI.getGiteaUserHeatmap(); if (res && res.ok) { remoteHeatmapUsername = res.username || ''; setRemoteHeatmapEntries(res.entries || []); remoteHeatmapFetchedAt = now; remoteHeatmapFetchState = 'ok'; return; } remoteHeatmapFetchState = 'error'; } catch (_) { remoteHeatmapFetchState = 'error'; } } function formatDateKey(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${y}-${m}-${d}`; } function startOfDay(date) { return new Date(date.getFullYear(), date.getMonth(), date.getDate()); } function addDays(date, days) { const n = new Date(date); n.setDate(n.getDate() + days); return n; } function shiftMonths(date, deltaMonths) { const d = new Date(date.getFullYear(), date.getMonth(), date.getDate()); d.setMonth(d.getMonth() + deltaMonths); return d; } function buildHeatmapData(monthsBack = activityHeatmapRangeMonths) { const today = startOfDay(new Date()); const from = startOfDay(shiftMonths(today, -(Math.max(1, Number(monthsBack) || 6)))); const fromDow = (from.getDay() + 6) % 7; // Monday=0 const gridStart = addDays(from, -fromDow); const counts = new Map(); let source = 'local'; if (remoteHeatmapFetchState === 'ok') { source = 'remote'; for (const [key, count] of remoteHeatmapCounts.entries()) { const day = startOfDay(new Date(key)); if (Number.isNaN(day.getTime()) || day < from || day > today) continue; counts.set(key, Math.max(0, Number(count) || 0)); } } else { for (const entry of activityEntries) { const ts = entry && entry.ts ? new Date(entry.ts) : null; if (!ts || Number.isNaN(ts.getTime())) continue; const day = startOfDay(ts); if (day < from || day > today) continue; const key = formatDateKey(day); counts.set(key, (counts.get(key) || 0) + 1); } } const totalDays = Math.floor((today - gridStart) / 86400000) + 1; const weekCount = Math.ceil(totalDays / 7); const weeks = []; let maxCount = 0; let total = 0; for (let w = 0; w < weekCount; w++) { const week = []; for (let d = 0; d < 7; d++) { const date = addDays(gridStart, w * 7 + d); const inRange = date >= from && date <= today; const key = formatDateKey(date); const count = inRange ? (counts.get(key) || 0) : 0; if (count > maxCount) maxCount = count; total += count; week.push({ date, inRange, count }); } weeks.push(week); } return { weeks, total, maxCount, source }; } function heatmapLevel(count, max) { if (!count) return 0; const q1 = Math.max(1, Math.ceil(max * 0.25)); const q2 = Math.max(q1 + 1, Math.ceil(max * 0.5)); const q3 = Math.max(q2 + 1, Math.ceil(max * 0.75)); if (count <= q1) return 1; if (count <= q2) return 2; if (count <= q3) return 3; return 4; } function renderActivityHeatmap(host) { if (!host) return; host.innerHTML = ''; const { weeks, total, maxCount, source } = buildHeatmapData(activityHeatmapRangeMonths); const card = document.createElement('section'); card.className = 'activity-heatmap-card' + (activityHeatmapCollapsed ? ' collapsed' : ''); const header = document.createElement('div'); header.className = 'activity-heatmap-header'; const title = document.createElement('strong'); title.textContent = 'Aktivitäts-Heatmap'; const toggleBtn = document.createElement('button'); toggleBtn.className = 'activity-heatmap-toggle'; toggleBtn.textContent = activityHeatmapCollapsed ? '▸ Ausklappen' : '▾ Einklappen'; toggleBtn.onclick = () => { activityHeatmapCollapsed = !activityHeatmapCollapsed; renderActivityHeatmap(host); }; const controls = document.createElement('div'); controls.className = 'activity-heatmap-controls'; controls.appendChild(toggleBtn); header.appendChild(title); header.appendChild(controls); card.appendChild(header); const body = document.createElement('div'); body.className = 'activity-heatmap-body'; const months = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']; const monthsRow = document.createElement('div'); monthsRow.className = 'activity-heatmap-months'; monthsRow.style.setProperty('--hm-weeks', String(weeks.length)); let prevMonth = -1; weeks.forEach((week, idx) => { const firstInRange = week.find(day => day.inRange); if (!firstInRange) return; const month = firstInRange.date.getMonth(); if (month !== prevMonth) { const label = document.createElement('span'); label.textContent = months[month]; label.style.gridColumn = `${idx + 1} / span 2`; monthsRow.appendChild(label); prevMonth = month; } }); const gridWrap = document.createElement('div'); gridWrap.className = 'activity-heatmap-grid-wrap'; const weekdays = document.createElement('div'); weekdays.className = 'activity-heatmap-weekdays'; ['Mo', '', 'Mi', '', 'Fr', '', ''].forEach(txt => { const el = document.createElement('span'); el.textContent = txt; weekdays.appendChild(el); }); const grid = document.createElement('div'); grid.className = 'activity-heatmap-grid'; grid.style.setProperty('--hm-weeks', String(weeks.length)); weeks.forEach(week => { const col = document.createElement('div'); col.className = 'activity-heatmap-week'; week.forEach(day => { const cell = document.createElement('div'); const level = day.inRange ? heatmapLevel(day.count, maxCount || 1) : 0; cell.className = `activity-heatmap-cell lv${level}` + (day.inRange ? '' : ' out'); cell.title = day.inRange ? `${day.count} Aktivitäten am ${day.date.toLocaleDateString('de-DE')}` : ''; col.appendChild(cell); }); grid.appendChild(col); }); gridWrap.appendChild(weekdays); gridWrap.appendChild(grid); const footer = document.createElement('div'); footer.className = 'activity-heatmap-footer'; const summary = document.createElement('span'); if (source === 'remote') { summary.textContent = `${total.toLocaleString('de-DE')} Beiträge vom Git-Profil in den letzten ${activityHeatmapRangeMonths} Monaten${remoteHeatmapUsername ? ` (${remoteHeatmapUsername})` : ''}`; } else { summary.textContent = `${total.toLocaleString('de-DE')} lokale Einträge in den letzten ${activityHeatmapRangeMonths} Monaten`; } const legend = document.createElement('div'); legend.className = 'activity-heatmap-legend'; legend.innerHTML = ` Weniger Mehr `; footer.appendChild(summary); footer.appendChild(legend); body.appendChild(monthsRow); body.appendChild(gridWrap); body.appendChild(footer); card.appendChild(body); host.appendChild(card); } function formatActivityTimestamp(iso) { try { return new Date(iso).toLocaleTimeString('de-DE', { hour12: false }); } catch (_) { return '--:--:--'; } } function renderActivityLog() { const list = $('activityLogList'); if (!list) return; const filter = ($('activityFilterLevel') && $('activityFilterLevel').value) || 'all'; const visible = activityEntries.filter(e => filter === 'all' || e.level === filter); if (visible.length === 0) { list.innerHTML = '
Noch keine Einträge.
'; return; } list.innerHTML = visible.map(e => { const lvl = (e.level || 'info').toUpperCase(); return `
${formatActivityTimestamp(e.ts)} ${lvl} ${escapeHtml(e.message)}
`; }).join(''); } function updateRetryQueueBadge(count) { retryQueueCount = Math.max(0, Number(count || 0)); const btn = $('btnRetryQueueNow'); if (btn) btn.textContent = `🔁 Queue (${retryQueueCount})`; const info = $('activityQueueInfo'); if (info) info.textContent = `Retry-Queue: ${retryQueueCount}`; } function parseBatchRepoInput(raw) { return String(raw || '') .split(/\r?\n/) .map(line => line.trim()) .filter(Boolean) .filter(line => line.includes('/')); } function renderInlineHint(id, text, tone = 'muted') { const el = $(id); if (!el) return; el.textContent = text || ''; el.classList.remove('error', 'success', 'warn'); if (tone === 'error') el.classList.add('error'); if (tone === 'success') el.classList.add('success'); if (tone === 'warn') el.classList.add('warn'); } function isRepoNameFormatValid(name) { return /^[a-zA-Z0-9](?:[a-zA-Z0-9._-]{0,98}[a-zA-Z0-9])?$/.test(String(name || '').trim()); } function findSimilarRepoNamesLocally(name) { const target = String(name || '').toLowerCase(); const normalizedTarget = target.replace(/[\s._-]+/g, ''); const names = (currentGiteaRepos || []) .map(r => String(r && r.name || '').trim()) .filter(Boolean); const exact = names.some(n => n.toLowerCase() === target); const similar = names .filter(n => { const lower = n.toLowerCase(); const normalized = lower.replace(/[\s._-]+/g, ''); return lower.includes(target) || target.includes(lower) || normalized.includes(normalizedTarget) || normalizedTarget.includes(normalized); }) .filter(n => n.toLowerCase() !== target) .slice(0, 8); return { exact, similar }; } async function validateRepoNameLive(name) { const value = String(name || '').trim(); if (!value) { renderInlineHint('repoNameValidationHint', 'Name prüfen: Duplikate, ähnliche Namen und ungültige Zeichen werden erkannt.', 'muted'); return { ok: true, blocking: false, existsExact: false, similar: [] }; } if (!isRepoNameFormatValid(value)) { renderInlineHint('repoNameValidationHint', 'Ungültiger Name. Erlaubt: Buchstaben, Zahlen, Punkt, Unterstrich, Bindestrich (1-100 Zeichen).', 'error'); return { ok: true, blocking: true, existsExact: false, similar: [] }; } let checkedRemotely = false; let existsExact = false; let similar = []; if (window.electronAPI.validateRepoName) { try { const platform = $('platform')?.value || 'gitea'; const res = await window.electronAPI.validateRepoName({ name: value, platform }); if (res && res.ok) { checkedRemotely = !!res.checked; existsExact = !!res.existsExact; similar = Array.isArray(res.similar) ? res.similar : []; } } catch (_) {} } if (!checkedRemotely) { const local = findSimilarRepoNamesLocally(value); existsExact = local.exact; similar = local.similar; } if (existsExact) { renderInlineHint('repoNameValidationHint', 'Dieses Repository existiert bereits. Bitte einen anderen Namen wählen.', 'error'); return { ok: true, blocking: true, existsExact, similar }; } if (similar.length > 0) { renderInlineHint('repoNameValidationHint', `Ähnliche Namen gefunden: ${similar.slice(0, 3).join(', ')}`, 'warn'); return { ok: true, blocking: false, existsExact, similar }; } renderInlineHint('repoNameValidationHint', 'Name ist frei und valide.', 'success'); return { ok: true, blocking: false, existsExact, similar }; } function scheduleRepoNameValidation() { if (repoNameValidationTimer) clearTimeout(repoNameValidationTimer); repoNameValidationTimer = setTimeout(() => { validateRepoNameLive($('repoName')?.value || ''); }, 180); } async function validateBatchCloneCollisions(strict = false) { const action = $('batchActionType')?.value || 'refresh'; if (action !== 'clone') { renderInlineHint('batchCloneValidationHint', 'Kollisionsprüfung aktiv: vorhandene Zielordner und Namenskonflikte werden angezeigt.', 'muted'); return true; } const repos = parseBatchRepoInput($('batchRepoList')?.value || ''); const targetDir = $('batchCloneTarget')?.value || ''; if (repos.length === 0) { renderInlineHint('batchCloneValidationHint', 'Keine Repositories eingetragen (Format: owner/repo).', strict ? 'error' : 'warn'); return !strict; } if (!targetDir) { renderInlineHint('batchCloneValidationHint', 'Bitte Zielordner wählen, um Kollisionen zu prüfen.', strict ? 'error' : 'warn'); return !strict; } if (!window.electronAPI.checkCloneTargetCollisions) { renderInlineHint('batchCloneValidationHint', 'Kollisionsprüfung nicht verfügbar.', strict ? 'error' : 'warn'); return !strict; } try { const res = await window.electronAPI.checkCloneTargetCollisions({ targetDir, repos }); if (!res || !res.ok) { renderInlineHint('batchCloneValidationHint', 'Kollisionsprüfung fehlgeschlagen.', strict ? 'error' : 'warn'); return !strict; } const dupes = Array.isArray(res.duplicateRepoNames) ? res.duplicateRepoNames : []; const existing = Array.isArray(res.existingTargets) ? res.existingTargets : []; if (dupes.length > 0 || existing.length > 0) { const duplicateText = dupes.length > 0 ? `Doppelte Repo-Namen: ${dupes.slice(0, 3).join(', ')}` : ''; const existingText = existing.length > 0 ? `Vorhandene Zielordner: ${existing.slice(0, 2).join(' | ')}` : ''; const joined = [duplicateText, existingText].filter(Boolean).join(' • '); renderInlineHint('batchCloneValidationHint', joined || 'Kollision erkannt.', 'error'); return false; } renderInlineHint('batchCloneValidationHint', 'Keine Kollisionen gefunden. Clone-Ziel ist sauber.', 'success'); return true; } catch (_) { renderInlineHint('batchCloneValidationHint', 'Kollisionsprüfung fehlgeschlagen.', strict ? 'error' : 'warn'); return !strict; } } function scheduleBatchCloneValidation() { if (batchCloneValidationTimer) clearTimeout(batchCloneValidationTimer); batchCloneValidationTimer = setTimeout(() => { validateBatchCloneCollisions(false); }, 220); } function updateBatchActionFields() { const action = $('batchActionType')?.value || 'refresh'; const cloneGroup = $('batchCloneGroup'); const tagGroup = $('batchTagGroup'); const releaseNameGroup = $('batchReleaseNameGroup'); const releaseBodyGroup = $('batchReleaseBodyGroup'); if (cloneGroup) cloneGroup.classList.toggle('hidden', action !== 'clone'); if (tagGroup) tagGroup.classList.toggle('hidden', !(action === 'create-tag' || action === 'create-release')); if (releaseNameGroup) releaseNameGroup.classList.toggle('hidden', action !== 'create-release'); if (releaseBodyGroup) releaseBodyGroup.classList.toggle('hidden', !(action === 'create-tag' || action === 'create-release')); } // 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; let repoSearchHotkeyBound = false; let settingsHealth = { url: 'Unbekannt', api: 'Unbekannt', auth: 'Unbekannt', latency: '-', version: '-', lastError: '-' }; function setHealthField(id, value) { const el = $(id); if (!el) return; el.textContent = value; el.classList.remove('health-ok', 'health-warn', 'health-error'); const v = (value || '').toLowerCase(); if (v === 'ok' || v === 'erreichbar' || v === 'gueltig' || v === 'gültig') { el.classList.add('health-ok'); } else if (v === 'fehler' || v === 'ungueltig' || v === 'ungültig') { el.classList.add('health-error'); } else if (v === 'unbekannt' || v === 'kein token' || v === 'token vorhanden' || v === 'nicht konfiguriert') { el.classList.add('health-warn'); } } function renderSettingsHealth() { setHealthField('healthUrl', settingsHealth.url); setHealthField('healthApi', settingsHealth.api); setHealthField('healthAuth', settingsHealth.auth); setHealthField('healthLatency', settingsHealth.latency); setHealthField('healthVersion', settingsHealth.version); setHealthField('healthLastError', settingsHealth.lastError); } function syncSettingsPanelHeights() { const credentialsPanel = document.querySelector('#settingsModal .settings-panel--credentials'); const healthPanel = document.querySelector('#settingsModal .settings-panel--health'); if (!credentialsPanel || !healthPanel) return; healthPanel.style.minHeight = ''; // In der einspaltigen Ansicht sollen die Karten natuerlich fliessen. if (window.matchMedia('(max-width: 1120px)').matches) return; const targetHeight = Math.ceil(credentialsPanel.getBoundingClientRect().height); if (targetHeight > 0) { healthPanel.style.minHeight = `${targetHeight}px`; } } function updateSettingsHealth(patch) { settingsHealth = { ...settingsHealth, ...patch }; renderSettingsHealth(); syncSettingsPanelHeights(); } function normalizeAndValidateGiteaUrl(rawUrl) { const value = (rawUrl || '').trim(); if (!value) return { ok: true, value: '' }; let parsed; try { parsed = new URL(value); } catch (_) { return { ok: false, error: 'Ungültige URL. Beispiel für IPv6: http://[2001:db8::1]:3000' }; } if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { return { ok: false, error: 'URL muss mit http:// oder https:// beginnen.' }; } return { ok: true, value: value.replace(/\/$/, '') }; } function renderGiteaUrlHint(rawValue) { const hint = $('giteaUrlHint'); if (!hint) return; const result = normalizeAndValidateGiteaUrl(rawValue); if (!rawValue || !rawValue.trim()) { hint.className = 'settings-inline-hint'; hint.textContent = 'Hinweis: IPv6 mit Klammern eingeben, z.B. http://[2001:db8::1]:3000'; return; } if (!result.ok) { hint.className = 'settings-inline-hint error'; hint.textContent = result.error; return; } hint.className = 'settings-inline-hint success'; hint.textContent = `Gültige URL: ${result.value}`; } function mapErrorMessage(message) { const raw = String(message || '').toLowerCase(); if (!raw) return 'Unbekannter Fehler'; if (raw.includes('401') || raw.includes('unauthorized') || raw.includes('authentifizierung')) { return 'Authentifizierung fehlgeschlagen. Bitte Token prüfen.'; } if (raw.includes('403') || raw.includes('forbidden') || raw.includes('zugriff verweigert')) { return 'Zugriff verweigert. Bitte Token-Berechtigungen prüfen.'; } if (raw.includes('404') || raw.includes('not found') || raw.includes('nicht gefunden')) { return 'Server oder Ressource nicht gefunden. URL/Repo prüfen.'; } if (raw.includes('econnrefused') || raw.includes('enotfound') || raw.includes('eai_again') || raw.includes('getaddrinfo')) { return 'Server nicht erreichbar. DNS, IPv4/IPv6 und Port prüfen.'; } if (raw.includes('timeout') || raw.includes('econnaborted') || raw.includes('zeitueberschreitung') || raw.includes('zeitüberschreitung')) { return 'Zeitüberschreitung bei der Verbindung. Bitte erneut versuchen.'; } if (raw.includes('ungueltige') || raw.includes('ungültige') || raw.includes('invalid') || raw.includes('url')) { return 'Ungültige URL. Beispiel für IPv6: http://[2001:db8::1]:3000'; } return String(message); } 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) { const friendly = mapErrorMessage(msg); updateSettingsHealth({ lastError: friendly }); setStatus(friendly); showToast(friendly, 'error'); logActivity('error', friendly); } function showSuccess(msg) { setStatus(msg); showToast(msg, 'success', 3000); logActivity('info', msg); } function showWarning(msg) { setStatus(msg); showToast(msg, 'warning'); logActivity('warning', msg); } function showInfo(msg) { setStatus(msg); showToast(msg, 'info', 2500); } function normalizeSearchText(value) { return String(value || '') .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, ''); } function fuzzyScoreToken(token, text) { if (!token || !text) return 0; const t = normalizeSearchText(text); const q = normalizeSearchText(token); if (!q) return 0; // Direkter Treffer bekommt starken Score const idx = t.indexOf(q); if (idx >= 0) { let score = 120 - Math.min(idx, 60); if (t.startsWith(q)) score += 20; return score; } // Fuzzy: alle Zeichen in Reihenfolge vorhanden let ti = 0; let gaps = 0; for (let qi = 0; qi < q.length; qi++) { const ch = q[qi]; const found = t.indexOf(ch, ti); if (found === -1) return 0; if (found > ti) gaps += (found - ti); ti = found + 1; } return Math.max(8, 80 - gaps * 2); } function getRepoCardSearchScore(card, query) { const raw = normalizeSearchText(query).trim(); if (!raw) return 1; const tokens = raw.split(/\s+/).filter(Boolean); const fields = [ card.dataset.searchName || '', card.dataset.searchOwner || '', card.dataset.searchFull || '', card.dataset.searchLanguage || '', card.dataset.searchTopics || '', card.dataset.searchDescription || '' ]; let total = 0; for (const token of tokens) { let best = 0; for (const field of fields) { best = Math.max(best, fuzzyScoreToken(token, field)); } if (best === 0) return 0; total += best; } return total; } function applyRepoFuzzyFilter(grid, searchInput, searchMetaEl) { if (!grid) return; const cards = Array.from(grid.querySelectorAll('.item-card')); const query = (searchInput?.value || '').trim(); if (!query) { cards.forEach(card => { card.style.display = 'flex'; card.style.order = ''; }); if (searchMetaEl) { searchMetaEl.textContent = `${cards.length} Repositories`; } return; } const ranked = cards .map(card => ({ card, score: getRepoCardSearchScore(card, query) })) .filter(entry => entry.score > 0) .sort((a, b) => b.score - a.score || a.card.dataset.searchName.localeCompare(b.card.dataset.searchName)); const visibleSet = new Set(ranked.map(entry => entry.card)); cards.forEach(card => { card.style.display = visibleSet.has(card) ? 'flex' : 'none'; }); ranked.forEach(entry => grid.appendChild(entry.card)); if (searchMetaEl) { const label = ranked.length === 1 ? 'Treffer' : 'Treffer'; searchMetaEl.textContent = `${ranked.length} ${label} für "${query}"`; } } function buildRepoWebUrl(owner, repoName) { const urlInput = $('giteaURL'); const normalized = normalizeAndValidateGiteaUrl(urlInput?.value || ''); if (!normalized.ok || !normalized.value) return null; return `${normalized.value}/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}`; } // 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 && !response.queued) { 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 if (response.ok && response.queued) { tab.originalContent = content; tab.dirty = false; pushToHistory(content); renderTabs(); showWarning(response.message || 'Änderung in Retry-Queue gelegt und wird später hochgeladen.'); updateRetryQueueBadge(retryQueueCount + 1); } 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...'); updateSettingsHealth({ lastError: '-' }); try { const res = await window.electronAPI.listGiteaRepos(); if (!res.ok) { showError('Failed to load repos: ' + (res.error || 'Unknown error')); updateSettingsHealth({ api: 'Fehler', auth: 'Fehler' }); return; } updateSettingsHealth({ api: 'Erreichbar', auth: 'OK', lastError: '-' }); currentGiteaRepos = Array.isArray(res.repos) ? res.repos : []; const grid = $('explorerGrid'); if (!grid) return; grid.innerHTML = ''; if (!res.repos || res.repos.length === 0) { renderFavHistorySidebar([]); grid.innerHTML = '
Keine Repositories gefunden
'; setStatus('No repositories found'); return; } // --- Fuzzy-Suchfeld für Repositories --- const searchContainer = document.createElement('div'); searchContainer.className = 'repo-search-wrap'; searchContainer.style.cssText = 'grid-column: 1/-1; margin-bottom: 20px;'; const searchTop = document.createElement('div'); searchTop.className = 'repo-search-top'; const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = '🔍 Fuzzy-Suche: Name, Owner, Sprache, Topics...'; searchInput.className = 'repo-search-input'; 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; `; const searchClearBtn = document.createElement('button'); searchClearBtn.className = 'secondary repo-search-clear'; searchClearBtn.textContent = '✕'; searchClearBtn.title = 'Suche leeren'; searchClearBtn.onclick = () => { searchInput.value = ''; applyRepoFuzzyFilter(grid, searchInput, null); searchInput.focus(); }; // Search Focus Effekt searchInput.addEventListener('focus', () => { searchInput.style.borderColor = 'var(--accent-primary)'; }); searchInput.addEventListener('blur', () => { searchInput.style.borderColor = 'rgba(255, 255, 255, 0.1)'; }); searchTop.appendChild(searchInput); searchTop.appendChild(searchClearBtn); searchContainer.appendChild(searchTop); grid.appendChild(searchContainer); const heatmapHost = document.createElement('div'); heatmapHost.id = 'repoActivityHeatmapHost'; heatmapHost.style.cssText = 'grid-column: 1/-1; margin-bottom: 18px;'; grid.appendChild(heatmapHost); await loadRemoteHeatmapData(); renderActivityHeatmap(heatmapHost); // Fuzzy Search Logic searchInput.addEventListener('input', (e) => { applyRepoFuzzyFilter(grid, e.target, null); }); searchInput.addEventListener('keydown', (e) => { if (e.key === 'Escape') { searchInput.value = ''; applyRepoFuzzyFilter(grid, searchInput, null); } }); if (!repoSearchHotkeyBound) { window.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { if (currentState.view !== 'gitea-list') return; const input = document.querySelector('.repo-search-input'); if (!input) return; e.preventDefault(); input.focus(); input.select(); } }); repoSearchHotkeyBound = true; } // ── Sidebar links einblenden ── renderFavHistorySidebar(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; card.dataset.owner = owner; card.dataset.repo = repoName; card.dataset.searchName = repoName || ''; card.dataset.searchOwner = owner || ''; card.dataset.searchFull = `${owner || ''}/${repoName || ''}`; card.dataset.searchLanguage = repo.language || ''; card.dataset.searchTopics = Array.isArray(repo.topics) ? repo.topics.join(' ') : ''; card.dataset.searchDescription = repo.description || ''; // 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); }); 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); }); applyRepoFuzzyFilter(grid, searchInput, null); setStatus(`Loaded ${res.repos.length} repos`); } catch (error) { console.error('Error loading repos:', error); showError('Error loading repositories'); updateSettingsHealth({ api: 'Fehler', auth: 'Unbekannt' }); } } 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; }; const addSep = () => { const sep = document.createElement('div'); sep.style.cssText = 'height:1px;background:rgba(255,255,255,0.08);margin:4px 0;'; menu.appendChild(sep); }; menu.appendChild(createMenuItem('📂', 'Repository öffnen', () => { menu.remove(); addToRecent(owner, repoName, cloneUrl); loadRepoContents(owner, repoName, ''); })); addSep(); menu.appendChild(createMenuItem('🔗', 'Clone-URL kopieren', async () => { menu.remove(); const res = await window.electronAPI.copyToClipboard(cloneUrl || ''); if (res?.ok) showInfo('Clone-URL kopiert'); else showError('Clone-URL konnte nicht kopiert werden'); })); menu.appendChild(createMenuItem('📋', 'owner/repo kopieren', async () => { menu.remove(); const res = await window.electronAPI.copyToClipboard(`${owner}/${repoName}`); if (res?.ok) showInfo('owner/repo kopiert'); else showError('owner/repo konnte nicht kopiert werden'); })); menu.appendChild(createMenuItem('🌐 Im Browser öffnen', async () => { menu.remove(); const url = buildRepoWebUrl(owner, repoName); if (!url) { showWarning('Gitea-URL fehlt oder ist ungültig. Bitte in den Settings prüfen.'); return; } const res = await window.electronAPI.openExternalUrl(url); if (res?.ok) showInfo('Repository im Browser geöffnet'); else showError('Browser konnte nicht geöffnet werden'); })); addSep(); // ── 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); addSep(); } 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) { showWarning('Repository-Name ist erforderlich.'); return; } const check = await validateRepoNameLive(name); if (!check.ok || check.blocking) { showError('Bitte den Repository-Namen korrigieren.'); return; } if (check.similar && check.similar.length > 0) { const proceed = confirm(`Ähnliche Repository-Namen gefunden: ${check.similar.slice(0, 3).join(', ')}\n\nTrotzdem erstellen?`); if (!proceed) 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 () => { initializePlatformSelection(); // Favoriten & Verlauf vorladen await loadFavoritesAndRecent(); renderSettingsHealth(); // 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 || ''; renderGiteaUrlHint(creds.giteaURL || ''); const checkedUrl = normalizeAndValidateGiteaUrl(creds.giteaURL || ''); updateSettingsHealth({ url: checkedUrl.ok && checkedUrl.value ? 'Gültig' : (checkedUrl.ok ? 'Leer' : 'Ungültig'), api: creds.giteaURL ? 'Unbekannt' : 'Nicht konfiguriert', auth: creds.giteaToken ? 'Token vorhanden' : 'Kein Token', latency: '-', version: '-' }); // 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); // Autostart-Status vom System lesen (Quelle der Wahrheit) try { const autostartRes = await window.electronAPI.getAutostart(); if (autostartRes && typeof autostartRes.enabled === 'boolean') { featureAutostart = autostartRes.enabled; } } catch (_) {} // 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; const cbAutostart = $('settingAutostart'); if (cbAutostart) cbAutostart.checked = featureAutostart; // 🆕 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'); renderGiteaUrlHint(''); updateSettingsHealth({ url: 'Nicht konfiguriert', api: 'Nicht konfiguriert', auth: 'Kein Token', latency: '-', version: '-', lastError: '-' }); } } 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 ($('btnWinMinimize')) $('btnWinMinimize').onclick = () => window.electronAPI.windowMinimize(); if ($('btnWinMaximize')) $('btnWinMaximize').onclick = () => window.electronAPI.windowMaximize(); if ($('btnWinClose')) $('btnWinClose').onclick = () => window.electronAPI.windowClose(); if ($('btnSettings')) { $('btnSettings').onclick = () => { $('settingsModal').classList.remove('hidden'); $('settingsWatermarkCard')?.classList.add('hidden'); renderSettingsHealth(); requestAnimationFrame(syncSettingsPanelHeights); }; } if ($('btnSettingsWatermark') && $('settingsWatermarkCard')) { $('btnSettingsWatermark').onclick = (e) => { e.stopPropagation(); $('settingsWatermarkCard').classList.toggle('hidden'); }; $('settingsWatermarkCard').addEventListener('click', (e) => { e.stopPropagation(); }); document.addEventListener('click', (e) => { if ($('settingsModal')?.classList.contains('hidden')) return; if ($('settingsWatermarkCard')?.classList.contains('hidden')) return; const target = e.target; if ($('btnSettingsWatermark')?.contains(target)) return; if ($('settingsWatermarkCard')?.contains(target)) return; $('settingsWatermarkCard')?.classList.add('hidden'); }); } window.addEventListener('resize', syncSettingsPanelHeights); if ($('btnBatchActions')) { $('btnBatchActions').onclick = () => { $('batchActionModal')?.classList.remove('hidden'); updateBatchActionFields(); scheduleBatchCloneValidation(); }; } if ($('btnOpenActivityLog')) { $('btnOpenActivityLog').onclick = () => { $('activityLogModal')?.classList.remove('hidden'); renderActivityLog(); }; } if ($('btnCloseActivityLog')) { $('btnCloseActivityLog').onclick = () => $('activityLogModal')?.classList.add('hidden'); } if ($('activityFilterLevel')) { $('activityFilterLevel').addEventListener('change', renderActivityLog); } if ($('btnClearActivityLog')) { $('btnClearActivityLog').onclick = () => { activityEntries = []; renderActivityLog(); refreshActivityHeatmapIfVisible(); }; } if ($('btnCloseBatchAction')) { $('btnCloseBatchAction').onclick = () => $('batchActionModal')?.classList.add('hidden'); } if ($('batchActionType')) { $('batchActionType').addEventListener('change', updateBatchActionFields); $('batchActionType').addEventListener('change', scheduleBatchCloneValidation); updateBatchActionFields(); } if ($('repoName')) { $('repoName').addEventListener('input', scheduleRepoNameValidation); $('repoName').addEventListener('blur', () => validateRepoNameLive($('repoName')?.value || '')); } if ($('batchRepoList')) { $('batchRepoList').addEventListener('input', scheduleBatchCloneValidation); } if ($('batchCloneTarget')) { $('batchCloneTarget').addEventListener('input', scheduleBatchCloneValidation); } if ($('btnSelectBatchCloneTarget')) { $('btnSelectBatchCloneTarget').onclick = async () => { const folder = await window.electronAPI.selectFolder(); if (folder && $('batchCloneTarget')) { $('batchCloneTarget').value = folder; scheduleBatchCloneValidation(); } }; } if ($('btnRunBatchAction')) { $('btnRunBatchAction').onclick = async () => { const action = $('batchActionType')?.value || 'refresh'; const repos = parseBatchRepoInput($('batchRepoList')?.value || ''); if (repos.length === 0) { showWarning('Bitte mindestens ein Repository im Format owner/repo eintragen.'); return; } const options = { cloneTargetDir: $('batchCloneTarget')?.value || '', tag: $('batchTagName')?.value || '', name: $('batchReleaseName')?.value || '', body: $('batchReleaseBody')?.value || '' }; if (action === 'clone' && !options.cloneTargetDir) { showWarning('Bitte zuerst einen Zielordner für Clone wählen.'); return; } if (action === 'clone') { const safeToClone = await validateBatchCloneCollisions(true); if (!safeToClone) { showError('Clone abgebrochen: Zielordner-Konflikt erkannt.'); return; } } if ((action === 'create-tag' || action === 'create-release') && !String(options.tag).trim()) { showWarning('Bitte einen Tag eintragen.'); return; } const btn = $('btnRunBatchAction'); const old = btn.textContent; btn.disabled = true; btn.textContent = 'Läuft...'; logActivity('info', `Batch gestartet: ${action} (${repos.length} Repos)`); try { const res = await window.electronAPI.runBatchRepoAction({ action, repos, options }); if (!res.ok) { showError(res.error || 'Batch-Aktion fehlgeschlagen'); return; } const summary = res.summary || { total: repos.length, success: 0, failed: 0 }; if (summary.failed > 0) { showWarning(`Batch beendet: ${summary.success}/${summary.total} erfolgreich, ${summary.failed} fehlgeschlagen.`); } else { showSuccess(`Batch erfolgreich: ${summary.success}/${summary.total}`); } (res.results || []).forEach(r => { if (r.ok) logActivity('info', `${r.repo}: ${r.message || 'OK'}`); else logActivity('error', `${r.repo}: ${r.error || 'Fehler'}`); }); } catch (error) { showError(error && error.message ? error.message : String(error)); } finally { btn.disabled = false; btn.textContent = old; } }; } if ($('btnRetryQueueNow')) { $('btnRetryQueueNow').onclick = async () => { try { const res = await window.electronAPI.processRetryQueueNow(); if (res.ok) { showSuccess(`Queue verarbeitet: ${res.succeeded || 0} erfolgreich, ${res.failed || 0} verworfen.`); } else { showWarning(res.error || 'Queue konnte nicht verarbeitet werden'); } } catch (e) { showError(e && e.message ? e.message : String(e)); } }; } if ($('btnRetryQueueRefresh')) { $('btnRetryQueueRefresh').onclick = async () => { try { const res = await window.electronAPI.processRetryQueueNow(); if (res.ok) { showSuccess(`Queue verarbeitet: ${res.succeeded || 0} erfolgreich, ${res.failed || 0} verworfen.`); } else { showWarning(res.error || 'Queue konnte nicht verarbeitet werden'); } } catch (e) { showError(e && e.message ? e.message : String(e)); } }; } if ($('giteaURL')) { $('giteaURL').addEventListener('input', (e) => { const raw = e.target.value; renderGiteaUrlHint(raw); const checked = normalizeAndValidateGiteaUrl(raw); updateSettingsHealth({ url: checked.ok && checked.value ? 'Gültig' : (checked.ok ? 'Leer' : 'Ungültig') }); }); } if ($('btnTestGiteaConnection')) { $('btnTestGiteaConnection').onclick = async () => { const token = $('giteaToken')?.value || ''; const rawUrl = $('giteaURL')?.value || ''; const checked = normalizeAndValidateGiteaUrl(rawUrl); if (!checked.ok) { showError(checked.error); updateSettingsHealth({ url: 'Ungültig', api: 'Unbekannt', auth: token ? 'Token vorhanden' : 'Kein Token' }); return; } if (!checked.value) { showWarning('Bitte zuerst eine Gitea URL eintragen.'); return; } setStatus('Teste Gitea-Verbindung...'); const btn = $('btnTestGiteaConnection'); const oldText = btn.textContent; btn.disabled = true; btn.textContent = 'Teste...'; try { const res = await window.electronAPI.testGiteaConnection({ token, url: checked.value, timeout: 8000 }); if (!res.ok) { showError(res.error || 'Verbindungstest fehlgeschlagen'); updateSettingsHealth({ url: 'Gültig', api: 'Fehler', auth: token ? 'Fehler' : 'Kein Token', latency: '-', version: '-' }); return; } const result = res.result || {}; const checks = result.checks || {}; const metrics = result.metrics || {}; const server = result.server || {}; updateSettingsHealth({ url: 'Gültig', api: checks.apiReachable ? 'Erreichbar' : 'Fehler', auth: checks.authProvided ? (checks.authOk ? 'OK' : 'Fehler') : 'Kein Token', latency: metrics.latencyMs ? `${metrics.latencyMs} ms` : '-', version: server.version || '-', lastError: '-' }); if (result.ok) showSuccess('Verbindung erfolgreich getestet'); else showWarning('Server erreichbar, aber Auth/Teilcheck fehlgeschlagen'); } catch (error) { console.error('test-gitea-connection error:', error); showError(error && error.message ? error.message : String(error)); } finally { btn.disabled = false; btn.textContent = oldText; } }; } if ($('btnCloseSettings')) { $('btnCloseSettings').onclick = () => { $('settingsWatermarkCard')?.classList.add('hidden'); $('settingsModal').classList.add('hidden'); }; } if ($('btnOpenRepoActions')) { $('btnOpenRepoActions').onclick = () => { $('repoActionModal').classList.remove('hidden'); scheduleRepoNameValidation(); }; } if ($('btnCloseRepoActions')) { $('btnCloseRepoActions').onclick = () => { $('repoActionModal').classList.add('hidden'); }; } try { const queue = await window.electronAPI.getRetryQueue(); if (queue && queue.ok) { updateRetryQueueBadge(queue.size || 0); logActivity('info', `Retry-Queue geladen (${queue.size || 0} Einträge)`); } } catch (_) {} 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 cbAutostart2 = $('settingAutostart'); const newAutostart = cbAutostart2 ? cbAutostart2.checked : false; if (newAutostart !== featureAutostart) { featureAutostart = newAutostart; await window.electronAPI.setAutostart(featureAutostart); } const checkedUrl = normalizeAndValidateGiteaUrl($('giteaURL').value); if (!checkedUrl.ok) { showError(checkedUrl.error); return; } const data = { githubToken: $('githubToken').value, giteaToken: $('giteaToken').value, giteaURL: checkedUrl.value, featureFavorites, featureRecent, compactMode, featureColoredIcons, favCollapsedFavorites: favSectionCollapsed.favorites, favCollapsedRecent: favSectionCollapsed.recent }; await window.electronAPI.saveCredentials(data); $('settingsModal').classList.add('hidden'); showSuccess('Settings saved'); renderGiteaUrlHint(checkedUrl.value); updateSettingsHealth({ url: checkedUrl.value ? 'Gültig' : 'Leer', auth: data.giteaToken ? 'Token vorhanden' : 'Kein Token', lastError: '-' }); // 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}`); }); if (window.electronAPI.onRetryQueueUpdated) { window.electronAPI.onRetryQueueUpdated((payload) => { const size = payload && typeof payload.size === 'number' ? payload.size : 0; updateRetryQueueBadge(size); if (payload && payload.event === 'queued' && payload.item) { const p = payload.item.payload || {}; logActivity('warning', `Queue: ${p.owner}/${p.repo}/${p.path} wurde eingeplant`); } else if (payload && payload.event === 'processed') { logActivity('info', `Queue-Retry: ${payload.succeeded || 0} erfolgreich, ${payload.failed || 0} verworfen`); } }); } if (window.electronAPI.onBatchActionProgress) { window.electronAPI.onBatchActionProgress((payload) => { if (!payload) return; if (payload.status === 'running') { logActivity('info', `Batch ${payload.action}: ${payload.repo} (${payload.index}/${payload.total})`); } else if (payload.status === 'error') { logActivity('error', `Batch ${payload.action}: ${payload.repo} - ${payload.error || 'Fehler'}`); } }); } // 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; } if (versionRes && versionRes.ok && $('watermarkVersion')) { $('watermarkVersion').textContent = versionRes.version; } } catch (error) { console.error('[Renderer] Fehler beim Laden der Version:', error); } if ($('watermarkCopyright')) { $('watermarkCopyright').textContent = `© ${new Date().getFullYear()} M_Viper`; } // 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); });