// renderer.js — Grid-UI + Navigation + Drag'n'Drop mit Fehlerbehandlung const $ = id => document.getElementById(id); let selectedFolder = null; let giteaCache = {}; let currentLocalProjects = []; /* ================================================ 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 = []; let repoPrivacyByFullName = {}; // owner/repo -> boolean(private) let repoTopicsByFullName = {}; // owner/repo -> string[] let repoKnownTopics = []; // all known topics across repos let repoKnownTopicsLoadedAt = 0; let currentGiteaUsername = ''; let activeRepoOwnerFilter = 'mine'; // mine | all | owner: function normalizePlatform(value) { return value === 'github' ? 'github' : 'gitea'; } function currentPlatformKey() { return normalizePlatform(currentState.platform); } function withPlatform(entry, fallback = 'gitea') { if (!entry || typeof entry !== 'object') return entry; return { ...entry, platform: normalizePlatform(entry.platform || fallback) }; } function platformEntries(items, platform = currentPlatformKey()) { const p = normalizePlatform(platform); return (Array.isArray(items) ? items : []).filter(e => normalizePlatform(e?.platform || 'gitea') === p); } async function loadFavoritesAndRecent() { try { const [favRes, recRes] = await Promise.all([ window.electronAPI.loadFavorites(), window.electronAPI.loadRecent() ]); if (favRes && favRes.ok) favorites = (favRes.favorites || []).map(e => withPlatform(e, 'gitea')); if (recRes && recRes.ok) recentRepos = (recRes.recent || []).map(e => withPlatform(e, 'gitea')); } catch(e) { console.error('loadFavoritesAndRecent:', e); } } function isFavorite(owner, repo, platform = currentPlatformKey()) { const p = normalizePlatform(platform); return favorites.some(f => f.owner === owner && f.repo === repo && normalizePlatform(f.platform || 'gitea') === p); } async function toggleFavorite(owner, repo, cloneUrl, platform = currentPlatformKey()) { const p = normalizePlatform(platform); if (isFavorite(owner, repo, p)) { favorites = favorites.filter(f => !(f.owner === owner && f.repo === repo && normalizePlatform(f.platform || 'gitea') === p)); } else { favorites.unshift({ owner, repo, cloneUrl, platform: p, addedAt: new Date().toISOString() }); } await window.electronAPI.saveFavorites(favorites); refreshFavHistoryUi(); } async function addToRecent(owner, repo, cloneUrl, platform = currentPlatformKey()) { if (!featureRecent) return; const p = normalizePlatform(platform); recentRepos = recentRepos.filter(r => !(r.owner === owner && r.repo === repo && normalizePlatform(r.platform || 'gitea') === p)); recentRepos.unshift({ owner, repo, cloneUrl, platform: p, 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'); } async function setPlatformSelection(platform) { currentState.platform = 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'); }); // Direkt laden; fehlende Credentials werden im Loader selbst sauber behandelt. loadRepos(); } function initializePlatformSelection() { const platformInput = $('platform'); const initialPlatform = platformInput?.value || 'gitea'; setPlatformSelection(initialPlatform); document.querySelectorAll('.platform-option').forEach(button => { button.addEventListener('click', () => { const p = button.dataset.platform || 'gitea'; if (p !== currentState.platform) { setPlatformSelection(p); } }); }); } /** Dispatches to the correct repo loader based on the active platform */ function loadRepos() { const requestId = ++repoLoadRequestId; if (currentState.platform === 'github') { loadGithubRepos(requestId); } else { loadGiteaRepos(null, requestId); } } /** Load repositories from GitHub and render the grid using loadGiteaRepos's renderer */ async function loadGithubRepos(requestId = null) { const activeRequestId = requestId || ++repoLoadRequestId; currentState.view = 'gitea-list'; currentState.path = ''; updateNavigationUI(); const btnCommits = $('btnCommits'); const btnReleases = $('btnReleases'); if (btnCommits) btnCommits.classList.add('hidden'); if (btnReleases) btnReleases.classList.add('hidden'); const grid = $('explorerGrid'); if (grid) grid.style.gridTemplateColumns = ''; setStatus('Loading GitHub repos...'); try { const res = await window.electronAPI.listGithubRepos(); if (activeRequestId !== repoLoadRequestId) return; if (!res.ok) { showError('GitHub: ' + (res.error || 'Unbekannter Fehler')); return; } // Normalize GitHub repo fields to match what the Gitea renderer expects const normalizedRepos = (Array.isArray(res.repos) ? res.repos : []).map(r => ({ ...r, stars_count: r.stargazers_count || 0, updated: r.updated_at || r.pushed_at || '', clone_url: r.clone_url || r.html_url || '' })); try { const cachedName = getCachedUsername('github'); if (cachedName) { currentGiteaUsername = cachedName; } else { const meRes = await window.electronAPI.getGithubCurrentUser(); if (activeRequestId !== repoLoadRequestId) return; currentGiteaUsername = meRes?.ok ? (meRes.user?.login || '') : ''; setCachedUsername('github', currentGiteaUsername); } } catch (_) { currentGiteaUsername = ''; } // Delegate rendering to loadGiteaRepos with pre-loaded data await loadGiteaRepos({ ok: true, repos: normalizedRepos }, activeRequestId); } catch (error) { if (activeRequestId !== repoLoadRequestId) return; console.error('Error loading GitHub repos:', error); showError('GitHub Fehler: ' + error.message); } } /* 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 currentPlatform = currentPlatformKey(); const favoritesCurrent = platformEntries(favorites, currentPlatform); const recentCurrent = platformEntries(recentRepos, currentPlatform); const items = activeType === 'favorites' ? favoritesCurrent : recentCurrent; 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 (${favoritesCurrent.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 (${recentCurrent.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, entry.platform || currentPlatform); 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.textContent = `${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, entry.platform || currentPlatformKey()); loadRepoContents(entry.owner, entry.repo, ''); }); const fullName = `${entry.owner || ''}/${entry.repo || ''}`; const isPrivate = !!repoPrivacyByFullName[fullName]; addItem( isPrivate ? '🌍' : '🔒', isPrivate ? 'Öffentlich machen' : 'Privat machen', async () => { await toggleRepoVisibility(entry.owner, entry.repo, isPrivate); } ); const chipTopics = repoTopicsByFullName[fullName] || []; addItem('🏷️', 'Tags bearbeiten', async () => { await editRepoTopics(entry.owner, entry.repo, chipTopics); }); // 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, entry.platform || currentPlatformKey()); loadGiteaRepos(); }, '#f59e0b'); } else { addItem('⭐', 'Zu Favoriten hinzufügen', async () => { await toggleFavorite(entry.owner, entry.repo, entry.cloneUrl, entry.platform || currentPlatformKey()); loadGiteaRepos(); }); addItem('✕', 'Aus Verlauf entfernen', async () => { const p = normalizePlatform(entry.platform || currentPlatformKey()); recentRepos = recentRepos.filter(r => !(r.owner === entry.owner && r.repo === entry.repo && normalizePlatform(r.platform || 'gitea') === p)); await window.electronAPI.saveRecent(recentRepos); loadGiteaRepos(); }, '#ef4444'); } document.body.appendChild(menu); setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10); } async function uploadDroppedPaths({ paths, owner, repo, destPath = '', cloneUrl = null, branch = 'HEAD' }) { try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:start', { owner, repo, destPath, branch, pathCount: Array.isArray(paths) ? paths.length : 0 }); } catch (_) {} console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:start', { owner, repo, destPath, branch, pathCount: Array.isArray(paths) ? paths.length : 0, platform: currentState.platform }); const safePaths = (Array.isArray(paths) ? paths : []).filter(Boolean); if (safePaths.length === 0) { try { window.electronAPI.debugToMain('warn', 'uploadDroppedPaths:no-safe-paths', { owner, repo, destPath, branch, rawPaths: paths }); } catch (_) {} return { ok: false, error: 'Keine gueltigen lokalen Dateipfade aus dem Drop ermittelt.' }; } if (currentState.platform === 'github') { return { ok: false, error: 'Drag-and-Drop Upload ist aktuell nur für Gitea aktiv.' }; } let successCount = 0; let failedCount = 0; const errors = []; const withTimeout = (promise, ms, label) => { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error(`${label}-timeout-${ms}ms`)), ms)) ]); }; for (let i = 0; i < safePaths.length; i++) { const p = safePaths[i]; const baseName = p.split(/[\\/]/).pop() || p; showProgress(Math.round(((i + 1) / safePaths.length) * 100), `Upload ${i + 1}/${safePaths.length}: ${baseName}`); try { let res = null; // Fast path: treat dropped item as file upload first. // If it is actually a directory, the call returns ok=false and we fall back to uploadAndPush. const fileTry = await withTimeout(window.electronAPI.uploadGiteaFile({ owner, repo, localPath: [p], destPath, branch, platform: currentState.platform }), 15000, 'upload-gitea-file'); console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:fileTry', { path: p, fileTry }); try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:fileTry', { path: p, fileTry }); } catch (_) {} const fileFailed = !fileTry?.ok || (Array.isArray(fileTry.results) && fileTry.results.some(r => !r.ok)); if (!fileFailed) { res = { ok: true, via: 'upload-gitea-file', results: fileTry.results || [] }; } else { res = await withTimeout(window.electronAPI.uploadAndPush({ localFolder: p, owner, repo, destPath, cloneUrl, branch }), 30000, 'upload-and-push'); } console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:itemResult', { path: p, res }); try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:itemResult', { path: p, res }); } catch (_) {} if (!res?.ok) { failedCount++; errors.push(`${baseName}: ${res?.error || 'Unbekannter Fehler'}`); continue; } const failedEntries = Array.isArray(res.results) ? res.results.filter(r => !r.ok) : []; if (failedEntries.length > 0) { failedCount++; errors.push(`${baseName}: ${failedEntries[0]?.error || 'Teilweise fehlgeschlagen'}`); } else { successCount++; } } catch (err) { failedCount++; const errMsg = String(err && err.message ? err.message : err); errors.push(`${baseName}: ${errMsg}`); console.error('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:itemError', { path: p, err: errMsg }); try { window.electronAPI.debugToMain('error', 'uploadDroppedPaths:itemError', { path: p, err: errMsg }); } catch (_) {} } } if (failedCount > 0) { return { ok: false, error: errors[0] || `${failedCount} Upload(s) fehlgeschlagen`, successCount, failedCount }; } const result = { ok: true, uploadedFiles: successCount, uploadedDirs: 0 }; try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:done', result); } catch (_) {} console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:done', result); return result; } function extractDroppedPaths(files) { const list = Array.isArray(files) ? files : Array.from(files || []); const out = []; for (const f of list) { const directPath = f && typeof f.path === 'string' ? f.path : ''; if (directPath) { out.push(directPath); continue; } let resolvedPath = ''; try { resolvedPath = window.electronAPI.getPathForFile ? window.electronAPI.getPathForFile(f) : ''; } catch (_) {} if (resolvedPath) out.push(resolvedPath); } return out.filter(Boolean); } async function toggleRepoVisibility(owner, repoName, currentPrivate) { try { const creds = await window.electronAPI.loadCredentials(); if (!creds?.giteaToken || !creds?.giteaURL) { showError('Gitea Token oder URL fehlt. Bitte zuerst in den Einstellungen speichern.'); return; } const targetPrivate = !currentPrivate; const actionText = targetPrivate ? 'privat' : 'oeffentlich'; showProgress(35, `Repository wird ${actionText} gesetzt...`); const result = await window.electronAPI.updateGiteaRepoVisibility({ token: creds.giteaToken, url: creds.giteaURL, owner, repo: repoName, isPrivate: targetPrivate }); hideProgress(); if (result?.ok) { showSuccess(`Repository ist jetzt ${actionText}.`); loadGiteaRepos(); } else { showError('Umschalten fehlgeschlagen: ' + (result?.error || 'Unbekannter Fehler')); } } catch (error) { hideProgress(); console.error('Visibility toggle error:', error); showError('Umschalten fehlgeschlagen'); } } function showTagsEditorModal(owner, repoName, seed, knownTopics) { return new Promise((resolve) => { const modal = document.createElement('div'); modal.className = 'modal'; modal.style.zIndex = '99999'; const initial = Array.isArray(seed) ? seed.filter(Boolean) : String(seed || '').split(',').map(t => t.trim()).filter(Boolean); const known = Array.from(new Set([...(knownTopics || []), ...initial])) .filter(Boolean) .sort((a, b) => a.localeCompare(b, 'de')); const card = document.createElement('div'); card.className = 'modalContent card'; card.style.maxWidth = '620px'; const title = document.createElement('h2'); title.textContent = '🏷️ Tags bearbeiten'; const group = document.createElement('div'); group.className = 'input-group'; const repoLabel = document.createElement('label'); repoLabel.textContent = `${owner}/${repoName}`; const selectedHostEl = document.createElement('div'); selectedHostEl.id = 'repoTagsSelected'; selectedHostEl.className = 'tags-editor-selected'; const row = document.createElement('div'); row.className = 'tags-editor-row'; const tagInput = document.createElement('input'); tagInput.id = 'repoTagInput'; tagInput.className = 'tags-editor-input'; tagInput.type = 'text'; tagInput.placeholder = 'Vorhandene Tags suchen oder neuen Tag eingeben'; tagInput.autocomplete = 'off'; const tagAddBtn = document.createElement('button'); tagAddBtn.id = 'btnRepoTagAdd'; tagAddBtn.className = 'tags-editor-add-btn'; tagAddBtn.textContent = 'Hinzufügen'; row.appendChild(tagInput); row.appendChild(tagAddBtn); const suggestionsHostEl = document.createElement('div'); suggestionsHostEl.id = 'repoTagSuggestions'; suggestionsHostEl.className = 'tags-editor-suggestions'; const hint = document.createElement('div'); hint.className = 'settings-inline-hint'; hint.textContent = 'Vorschlaege kommen live von deiner Gitea-Seite. Neue Tags sind ebenfalls erlaubt.'; group.appendChild(repoLabel); group.appendChild(selectedHostEl); group.appendChild(row); group.appendChild(suggestionsHostEl); group.appendChild(hint); const buttons = document.createElement('div'); buttons.className = 'modal-buttons'; buttons.style.marginTop = '16px'; const saveButton = document.createElement('button'); saveButton.id = 'btnRepoTagsSave'; saveButton.className = 'accent-btn'; saveButton.textContent = 'Speichern'; const cancelButton = document.createElement('button'); cancelButton.id = 'btnRepoTagsCancel'; cancelButton.className = 'secondary'; cancelButton.textContent = 'Abbrechen'; buttons.appendChild(saveButton); buttons.appendChild(cancelButton); card.appendChild(title); card.appendChild(group); card.appendChild(buttons); modal.appendChild(card); document.body.appendChild(modal); const selectedHost = modal.querySelector('#repoTagsSelected'); const input = modal.querySelector('#repoTagInput'); const addBtn = modal.querySelector('#btnRepoTagAdd'); const suggestionsHost = modal.querySelector('#repoTagSuggestions'); const selected = [...initial]; const renderSelected = () => { if (!selectedHost) return; if (selected.length === 0) { selectedHost.innerHTML = 'Noch keine Tags gesetzt'; return; } selectedHost.innerHTML = ''; selected.forEach((tag, idx) => { const tagChip = document.createElement('span'); tagChip.style.cssText = 'display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;border:1px solid rgba(88,213,255,0.35);background:rgba(88,213,255,0.12);font-size:12px;'; tagChip.appendChild(document.createTextNode(String(tag || ''))); const removeBtn = document.createElement('button'); removeBtn.style.cssText = 'all:unset;cursor:pointer;font-size:12px;opacity:0.85;'; removeBtn.textContent = '✕'; removeBtn.onclick = () => { if (Number.isInteger(idx) && idx >= 0 && idx < selected.length) { selected.splice(idx, 1); renderSelected(); } }; tagChip.appendChild(removeBtn); selectedHost.appendChild(tagChip); }); }; const addTag = (rawTag) => { const tag = String(rawTag || '').trim(); if (!tag) return; if (selected.length >= 30) return; const exists = selected.some(t => t.toLowerCase() === tag.toLowerCase()); if (!exists) selected.push(tag); renderSelected(); }; const renderSuggestions = (queryRaw) => { if (!suggestionsHost) return; const query = String(queryRaw || '').trim().toLowerCase(); let list = known; if (query) { list = known.filter(t => t.toLowerCase().includes(query)); } list = list.filter(t => !selected.some(s => s.toLowerCase() === t.toLowerCase())).slice(0, 50); if (list.length === 0) { suggestionsHost.innerHTML = '
Keine Vorschlaege
'; return; } suggestionsHost.innerHTML = ''; list.forEach(tag => { const btn = document.createElement('button'); btn.className = 'tags-editor-suggestion-item'; btn.textContent = String(tag || ''); btn.onclick = () => { addTag(tag); if (input) { input.value = ''; input.focus(); } renderSuggestions(''); }; suggestionsHost.appendChild(btn); }); }; const closeWith = (value) => { modal.remove(); resolve(value); }; if (input) { input.focus(); input.addEventListener('input', () => renderSuggestions(input.value)); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addTag(input.value); input.value = ''; renderSuggestions(''); } if (e.key === 'Escape') closeWith(null); }); } renderSelected(); renderSuggestions(''); const saveBtn = modal.querySelector('#btnRepoTagsSave'); const cancelBtn = modal.querySelector('#btnRepoTagsCancel'); if (addBtn) addBtn.onclick = (e) => { e.preventDefault(); addTag(input ? input.value : ''); if (input) { input.value = ''; input.focus(); } renderSuggestions(''); }; if (saveBtn) saveBtn.onclick = () => { if (input && input.value.trim()) addTag(input.value.trim()); closeWith(selected.slice(0, 30)); }; if (cancelBtn) cancelBtn.onclick = () => closeWith(null); modal.onclick = (e) => { if (e.target === modal) closeWith(null); }; }); } async function fetchKnownTopicsFromGitea(force = false) { const now = Date.now(); if (!force && repoKnownTopics.length > 0 && (now - repoKnownTopicsLoadedAt) < 5 * 60 * 1000) { return repoKnownTopics; } if (!window.electronAPI.getGiteaTopicsCatalog) return repoKnownTopics; try { const res = await window.electronAPI.getGiteaTopicsCatalog(); if (res?.ok && Array.isArray(res.topics)) { repoKnownTopics = res.topics; repoKnownTopicsLoadedAt = now; } } catch (_) {} return repoKnownTopics; } async function editRepoTopics(owner, repoName, currentTopics = []) { const seed = Array.isArray(currentTopics) ? currentTopics : []; const known = await fetchKnownTopicsFromGitea(); const raw = await showTagsEditorModal(owner, repoName, seed, known); if (raw === null) return; const topics = Array.isArray(raw) ? raw : []; showProgress(35, 'Tags werden aktualisiert...'); const result = await window.electronAPI.updateGiteaRepoTopics({ owner, repo: repoName, topics }); hideProgress(); if (result?.ok) { showSuccess('Tags aktualisiert'); loadGiteaRepos(); } else { showError('Tags konnten nicht gespeichert werden: ' + (result?.error || 'Unbekannter Fehler')); } } // 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: '', platform: 'gitea' // 'gitea' | 'github' }; let repoLoadRequestId = 0; const USER_CACHE_MS = 5 * 60 * 1000; let currentUserCache = { gitea: { name: '', ts: 0 }, github: { name: '', ts: 0 } }; function getCachedUsername(platform) { const p = platform === 'github' ? 'github' : 'gitea'; const item = currentUserCache[p]; if (!item?.name) return ''; if ((Date.now() - Number(item.ts || 0)) > USER_CACHE_MS) return ''; return item.name; } function setCachedUsername(platform, name) { const p = platform === 'github' ? 'github' : 'gitea'; currentUserCache[p] = { name: String(name || ''), ts: Date.now() }; } 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 = ''; let remoteHeatmapPlatform = ''; let remoteHeatmapMonths = 0; function getActiveHeatmapMonths() { return activityHeatmapRangeMonths; } 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; } function hasPositiveHeatmapEntries(entries) { return Array.isArray(entries) && entries.some(item => Number(item?.count || 0) > 0); } async function buildGithubHeatmapFromRepoCommits(monthsBack) { if (currentPlatformKey() !== 'github') return []; const repos = Array.isArray(currentGiteaRepos) ? currentGiteaRepos.slice(0, 60) : []; if (repos.length === 0) return []; const since = new Date(); since.setMonth(since.getMonth() - Math.max(1, Number(monthsBack) || 20)); const sinceTs = since.getTime(); const myLogin = String(currentGiteaUsername || '').trim().toLowerCase(); const dayMap = new Map(); const concurrency = 4; let repoIndex = 0; const worker = async () => { while (repoIndex < repos.length) { const repo = repos[repoIndex++]; const owner = repo?.owner?.login || repo?.owner?.username; const repoName = repo?.name; if (!owner || !repoName) continue; try { const res = await window.electronAPI.getCommits({ owner, repo: repoName, branch: repo?.default_branch || 'HEAD', limit: 100, platform: 'github' }); if (!res?.ok || !Array.isArray(res.commits)) continue; for (const commit of res.commits) { const authorLogin = String(commit?.author?.login || '').toLowerCase(); if (myLogin && authorLogin && authorLogin !== myLogin) continue; const dateValue = commit?.commit?.author?.date || commit?.commit?.committer?.date; if (!dateValue) continue; const ts = new Date(dateValue).getTime(); if (!Number.isFinite(ts) || ts < sinceTs) continue; const key = normalizeHeatmapEntryDate(dateValue); if (!key) continue; dayMap.set(key, (dayMap.get(key) || 0) + 1); } } catch (_) { // einzelne Repos koennen fehlschlagen ohne den Gesamtprozess zu blockieren } } }; await Promise.all(Array.from({ length: concurrency }, () => worker())); return Array.from(dayMap.entries()) .map(([date, count]) => ({ date, count })) .sort((a, b) => a.date.localeCompare(b.date)); } async function loadRemoteHeatmapData(force = false) { const platform = currentState.platform === 'github' ? 'github' : 'gitea'; const monthsBack = getActiveHeatmapMonths(); const now = Date.now(); if (!force && remoteHeatmapFetchState === 'ok' && remoteHeatmapPlatform === platform && remoteHeatmapMonths === monthsBack && (now - remoteHeatmapFetchedAt) < HEATMAP_CACHE_MS) { return; } const loadFn = platform === 'github' ? window.electronAPI?.getGithubUserHeatmap : window.electronAPI?.getGiteaUserHeatmap; if (!window.electronAPI || typeof loadFn !== 'function') { remoteHeatmapFetchState = 'error'; remoteHeatmapPlatform = platform; remoteHeatmapMonths = monthsBack; remoteHeatmapUsername = ''; setRemoteHeatmapEntries([]); return; } try { const res = await loadFn({ monthsBack }); if (res && res.ok) { remoteHeatmapUsername = res.username || currentGiteaUsername || ''; let entries = Array.isArray(res.entries) ? res.entries : []; if (platform === 'github' && !hasPositiveHeatmapEntries(entries)) { entries = await buildGithubHeatmapFromRepoCommits(monthsBack); } setRemoteHeatmapEntries(entries); remoteHeatmapFetchedAt = now; remoteHeatmapFetchState = 'ok'; remoteHeatmapPlatform = platform; remoteHeatmapMonths = monthsBack; return; } remoteHeatmapFetchState = 'error'; remoteHeatmapPlatform = platform; remoteHeatmapMonths = monthsBack; remoteHeatmapUsername = ''; setRemoteHeatmapEntries([]); } catch (_) { remoteHeatmapFetchState = 'error'; remoteHeatmapPlatform = platform; remoteHeatmapMonths = monthsBack; remoteHeatmapUsername = ''; setRemoteHeatmapEntries([]); } } 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 monthsBack = getActiveHeatmapMonths(); const { weeks, total, maxCount, source } = buildHeatmapData(monthsBack); // Dynamische Zellgröße: passt die Heatmap an die verfügbare Kartenbreite an, // damit rechts keine Monate/Tage abgeschnitten werden. const weekCount = Math.max(1, weeks.length); const weekdayCol = 22; const gridGap = 8; const weekGap = 2; const hostWidth = Math.max(320, host.clientWidth || 0); const horizontalPadding = 24; const availableForGrid = Math.max(220, hostWidth - horizontalPadding - weekdayCol - gridGap); const computedCell = (availableForGrid - ((weekCount - 1) * weekGap)) / weekCount; const cellSize = Math.max(6, Math.min(12, Number(computedCell.toFixed(2)))); 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'; body.style.setProperty('--hm-cell-size', `${cellSize}px`); body.style.setProperty('--hm-week-gap', `${weekGap}px`); 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') { const profileLabel = currentState.platform === 'github' ? 'GitHub-Profil' : 'Git-Profil'; summary.textContent = `${total.toLocaleString('de-DE')} Beiträge vom ${profileLabel} in den letzten ${monthsBack} Monaten${remoteHeatmapUsername ? ` (${remoteHeatmapUsername})` : ''}`; } else { summary.textContent = `${total.toLocaleString('de-DE')} lokale Einträge in den letzten ${monthsBack} Monaten`; } const legend = document.createElement('div'); legend.className = 'activity-heatmap-legend'; const less = document.createElement('span'); less.textContent = 'Weniger'; legend.appendChild(less); [0, 1, 2, 3, 4].forEach((lv) => { const cell = document.createElement('i'); cell.className = `activity-heatmap-cell lv${lv}`; legend.appendChild(cell); }); const more = document.createElement('span'); more.textContent = 'Mehr'; legend.appendChild(more); 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 = ''; const row = document.createElement('div'); row.className = 'activity-log-item info'; const msg = document.createElement('span'); msg.className = 'activity-log-message'; msg.textContent = 'Noch keine Einträge.'; row.appendChild(msg); list.appendChild(row); return; } list.innerHTML = ''; for (const e of visible) { const row = document.createElement('div'); row.className = `activity-log-item ${e.level || 'info'}`; const time = document.createElement('span'); time.className = 'activity-log-time'; time.textContent = formatActivityTimestamp(e.ts); const level = document.createElement('span'); level.className = 'activity-log-level'; level.textContent = (e.level || 'info').toUpperCase(); const msg = document.createElement('span'); msg.className = 'activity-log-message'; msg.textContent = String(e.message || ''); row.appendChild(time); row.appendChild(level); row.appendChild(msg); list.appendChild(row); } } 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, rawToken = '') { const hint = $('giteaUrlHint'); if (!hint) return; const url = String(rawValue || '').trim(); const token = String(rawToken || '').trim(); const result = normalizeAndValidateGiteaUrl(url); const connected = !!token && !!url && !!result.ok; hint.className = `settings-inline-hint ${connected ? 'success' : 'error'}`; hint.textContent = connected ? 'Verbunden' : 'Nicht verbunden'; } function renderGithubTokenHint(rawToken) { const hint = $('githubTokenHint'); if (!hint) return; const token = String(rawToken || '').trim(); const connected = !!token; hint.className = `settings-inline-hint ${connected ? 'success' : 'error'}`; hint.textContent = connected ? 'Verbunden' : 'Nicht verbunden'; } 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(); const isOwnerMatch = (card) => { const owner = String(card.dataset.searchOwner || '').toLowerCase(); const isShared = String(card.dataset.shared || '') === 'true'; if (activeRepoOwnerFilter === 'all') return true; if (activeRepoOwnerFilter === 'mine') { if (!currentGiteaUsername) return true; return owner === String(currentGiteaUsername).toLowerCase(); } if (activeRepoOwnerFilter === 'shared') { return isShared; } if (activeRepoOwnerFilter.startsWith('owner:')) { const target = activeRepoOwnerFilter.slice('owner:'.length).toLowerCase(); return owner === target; } return true; }; if (!query) { cards.forEach(card => { card.style.display = isOwnerMatch(card) ? 'flex' : 'none'; card.style.order = ''; }); if (searchMetaEl) { const visibleCount = cards.filter(card => isOwnerMatch(card)).length; searchMetaEl.textContent = `${visibleCount} Repositories`; } return; } const ranked = cards .filter(card => isOwnerMatch(card)) .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 isRepoWritable(repo, currentUsername) { const owner = String(repo?.owner?.login || repo?.owner?.username || '').toLowerCase(); const me = String(currentUsername || '').toLowerCase(); if (owner && me && owner === me) return true; const perms = repo?.permissions || repo?.permission || {}; if (perms.admin === true) return true; if (perms.push === true) return true; return false; } 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, platform = null) { openTabs[filePath] = { name: fileName, content: content, originalContent: content, dirty: false, icon: getFileIcon(fileName), isGitea, owner, repo, platform: platform || (isGitea ? currentState.platform : null), 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', async (e) => { e.stopPropagation(); if (tab.dirty) { const ok = await showActionConfirmModal({ title: 'Ungespeicherte Aenderungen', message: `${tab.name} hat ungespeicherte Aenderungen. Wirklich schliessen?`, confirmText: 'Schliessen', danger: true }); if (!ok) 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), platform: tab.platform || 'gitea' }); 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); } } async function closeFileEditor() { // Überprüfe auf ungespeicherte Änderungen const unsaved = Object.entries(openTabs).filter(([_, tab]) => tab.dirty); if (unsaved.length > 0) { const ok = await showActionConfirmModal({ title: 'Ungespeicherte Aenderungen', message: `${unsaved.length} Datei(en) haben ungespeicherte Aenderungen. Wirklich schliessen?`, confirmText: 'Schliessen', danger: true }); if (!ok) 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 { await showInfoModal('Datei konnte nicht geoeffnet werden', `Fehler: ${response.error || 'Unbekannter Fehler'}`, true); 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); await showInfoModal('Datei konnte nicht geoeffnet werden', 'Fehler beim Oeffnen der Datei.', true); } } 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/GitHub Handler const response = await window.electronAPI.readGiteaFile({ owner, repo, path: filePath, ref: getDefaultBranch(owner, repo), platform: currentState.platform }); if (response.ok) { addTab(vPath, fileName, response.content, true, owner, repo, currentState.platform); const modal = $('fileEditorModal'); if (modal) { modal.classList.remove('hidden'); initEditor(); $('fileEditorContent').focus(); } setStatus(`Editiere: ${fileName}`); console.log('✅ Gitea file opened'); } else { await showInfoModal('Datei konnte nicht geladen werden', `Fehler: ${response.error || 'Unbekannter Fehler'}`, true); showError('Fehler beim Laden der Datei'); } } catch (error) { console.error('Error opening Gitea file:', error); await showInfoModal('Datei konnte nicht geoeffnet werden', 'Fehler beim Oeffnen der Datei.', true); 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)) { await showInfoModal('Nicht bearbeitbar', 'Bilder koennen nicht bearbeitet werden.'); return; } const textarea = $('fileEditorContent'); const content = textarea.value; if (!isAutoSave) setStatus('Speichert...'); try { let response; // Prüfe ob es eine Gitea/GitHub-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), platform: tab.platform || 'gitea' }); } 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 { await showInfoModal('Speichern fehlgeschlagen', `Fehler: ${response.error || 'Unbekannter Fehler'}`, true); } } catch (error) { console.error('Error saving file:', error); await showInfoModal('Speichern fehlgeschlagen', 'Fehler beim Speichern.', true); } } 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 ''; // Immer escapen: verhindert Script-/HTML-Injection aus Release-Texten. let html = String(markdown) .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(preloadedData = null, requestId = null) { const activeRequestId = requestId || ++repoLoadRequestId; 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 repos...'); if (!preloadedData) updateSettingsHealth({ lastError: '-' }); try { let res; if (preloadedData) { res = preloadedData; } else { res = await window.electronAPI.listGiteaRepos(); if (activeRequestId !== repoLoadRequestId) return; 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 : []; if (!preloadedData) { try { const cachedName = getCachedUsername('gitea'); if (cachedName) { currentGiteaUsername = cachedName; } else { const meRes = await window.electronAPI.getGiteaCurrentUser?.(); if (activeRequestId !== repoLoadRequestId) return; currentGiteaUsername = meRes?.ok ? (meRes.user?.login || meRes.user?.username || '') : ''; setCachedUsername('gitea', currentGiteaUsername); } } catch (_) { currentGiteaUsername = ''; } } 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 ownerTabsWrap = document.createElement('div'); ownerTabsWrap.className = 'repo-owner-tabs'; ownerTabsWrap.style.cssText = 'grid-column: 1/-1; margin-bottom: 12px;'; const ownerCounts = new Map(); for (const r of currentGiteaRepos) { const owner = (r?.owner?.login || r?.owner?.username || '').trim(); if (!owner) continue; ownerCounts.set(owner, (ownerCounts.get(owner) || 0) + 1); } const myOwner = String(currentGiteaUsername || '').trim(); const tabDefs = [ { key: 'mine', label: 'Meine', count: myOwner ? (ownerCounts.get(myOwner) || 0) : currentGiteaRepos.length }, { key: 'all', label: 'Alle', count: currentGiteaRepos.length } ]; Array.from(ownerCounts.keys()) .filter(o => !myOwner || o.toLowerCase() !== myOwner.toLowerCase()) .sort((a, b) => a.localeCompare(b, 'de')) .forEach(owner => tabDefs.push({ key: `owner:${owner}`, label: owner, count: ownerCounts.get(owner) || 0 })); if (activeRepoOwnerFilter === 'mine' && tabDefs[0].count === 0) { activeRepoOwnerFilter = 'all'; } if (activeRepoOwnerFilter === 'shared') { activeRepoOwnerFilter = 'all'; } const makeTab = (tab) => { const btn = document.createElement('button'); btn.className = 'repo-owner-tab' + (activeRepoOwnerFilter === tab.key ? ' active' : ''); btn.textContent = String(tab.label || ''); btn.appendChild(document.createTextNode(' ')); const countSpan = document.createElement('span'); countSpan.textContent = String(tab.count || 0); btn.appendChild(countSpan); btn.onclick = () => { activeRepoOwnerFilter = tab.key; ownerTabsWrap.querySelectorAll('.repo-owner-tab').forEach(el => { el.classList.toggle('active', el === btn); }); applyRepoFuzzyFilter(grid, searchInput, null); }; return btn; }; tabDefs.forEach(tab => ownerTabsWrap.appendChild(makeTab(tab))); grid.appendChild(ownerTabsWrap); 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); renderActivityHeatmap(heatmapHost); loadRemoteHeatmapData(false).finally(() => { if (activeRequestId !== repoLoadRequestId) return; 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); repoPrivacyByFullName = {}; repoTopicsByFullName = {}; const knownTopicSet = new Set(); 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; repoPrivacyByFullName[`${owner || ''}/${repoName || ''}`] = !!repo.private; const repoTopics = Array.isArray(repo.topics) ? repo.topics : []; repoTopicsByFullName[`${owner || ''}/${repoName || ''}`] = repoTopics; repoTopics.forEach(t => { const s = String(t || '').trim(); if (s) knownTopicSet.add(s); }); // 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 || ''; const ownerLower = String(owner || '').toLowerCase(); const myOwnerLower = String(currentGiteaUsername || '').toLowerCase(); const sharedByOthers = !!ownerLower && !!myOwnerLower && ownerLower !== myOwnerLower; card.dataset.shared = sharedByOthers ? 'true' : 'false'; card.dataset.searchSharedOwner = sharedByOthers ? `geteilt von ${owner}` : ''; const writable = isRepoWritable(repo, currentGiteaUsername); const readOnly = !writable; card.dataset.readOnly = readOnly ? 'true' : 'false'; // 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'; if (repo.avatar_url) { const avatarImg = document.createElement('img'); avatarImg.src = repo.avatar_url; avatarImg.className = 'repo-avatar-img'; avatarImg.alt = ''; avatarImg.onerror = () => { iconEl.textContent = '📦'; avatarImg.remove(); }; iconEl.appendChild(avatarImg); } else { iconEl.textContent = '📦'; } card.appendChild(iconEl); const nameEl = document.createElement('div'); nameEl.className = 'item-name'; nameEl.textContent = repoName; card.appendChild(nameEl); if (sharedByOthers) { const sharedByEl = document.createElement('div'); sharedByEl.className = 'item-submeta'; sharedByEl.textContent = `geteilt von ${owner}`; card.appendChild(sharedByEl); } if (readOnly) { const roBadge = document.createElement('div'); roBadge.className = 'repo-size-badge'; roBadge.style.top = '10px'; roBadge.style.right = '42px'; roBadge.style.background = 'rgba(59,130,246,0.16)'; roBadge.style.borderColor = 'rgba(59,130,246,0.45)'; roBadge.textContent = 'Nur Ansicht'; card.appendChild(roBadge); } // 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; try { window.electronAPI.debugToMain('log', 'repoCard:drop:event', { owner, repo: repoName, fileCount: files ? files.length : 0, itemCount: ev.dataTransfer?.items ? ev.dataTransfer.items.length : 0 }); } catch (_) {} if (!files || files.length === 0) { showWarning("Keine Dateien zum Upload gefunden."); return; } const paths = extractDroppedPaths(files); if (paths.length === 0) { try { window.electronAPI.debugToMain('warn', 'repoCard:drop:no-paths', { fileNames: Array.from(files).map(f => f.name) }); } catch (_) {} showError('Dateipfade konnten nicht gelesen werden (Sandbox/Drag-Quelle). Bitte Upload über Kontextmenü testen.'); return; } console.log('[UPLOAD_DEBUG][renderer] repoCard:drop', { owner, repo: repoName, branch: getDefaultBranch(owner, repoName), paths }); setStatus(`Starte Upload von ${paths.length} Elementen...`); try { const res = await uploadDroppedPaths({ paths, owner, repo: repoName, destPath: '', cloneUrl, branch: getDefaultBranch(owner, repoName) }); console.log('[UPLOAD_DEBUG][renderer] repoCard:dropResult', res); if (!res.ok) { showError('Fehler: ' + (res.error || 'Upload fehlgeschlagen')); setStatus('Upload fehlgeschlagen'); } else { setStatus('Upload abgeschlossen'); } } catch (err) { console.error('Kritischer Upload Fehler:', err); setStatus('Upload fehlgeschlagen'); } hideProgress(); }); card.onclick = () => { addToRecent(owner, repoName, cloneUrl); loadRepoContents(owner, repoName, ''); }; card.oncontextmenu = (ev) => showRepoContextMenu(ev, owner, repoName, cloneUrl, card, !!repo.private, repo); grid.appendChild(card); }); repoKnownTopics = Array.from(knownTopicSet).sort((a, b) => a.localeCompare(b, 'de')); repoKnownTopicsLoadedAt = Date.now(); 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); const platform = currentState.platform; try { const res = await window.electronAPI.getGiteaRepoContents({ owner, repo, path, ref, platform }); 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'; const emptyEl = document.createElement('div'); emptyEl.style.cssText = 'grid-column: 1/-1; text-align: center; padding: 40px; color: var(--text-muted);'; emptyEl.textContent = emptyMsg; grid.appendChild(emptyEl); 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 = extractDroppedPaths(files); if (paths.length === 0) { showError('Dateipfade konnten nicht gelesen werden (Sandbox/Drag-Quelle).'); return; } const targetPath = item.path; try { const res = await uploadDroppedPaths({ paths, owner, repo, destPath: targetPath, branch: getDefaultBranch(owner, repo) }); if (!res.ok) { showError('Upload error: ' + (res.error || 'Unbekannter Fehler')); } } catch (error) { console.error('Upload error:', error); showError('Upload failed'); } 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 }); currentLocalProjects = Array.isArray(res.tree) ? res.tree.filter(node => node && node.isDirectory) : []; const grid = $('explorerGrid'); if (!grid) return; grid.innerHTML = ''; if (!res.ok) { showError('Error loading local files'); return; } if (!res.tree || res.tree.length === 0) { currentLocalProjects = []; 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) { await showActionConfirmModal({ title: 'Lokaler Ordner fehlt', message: 'Bitte waehle zuerst einen lokalen Ordner aus.', confirmText: 'OK', danger: false }); 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) { showSuccess('Upload erfolgreich ✓'); } else { showError('Upload fehlgeschlagen: ' + (res.error || 'Unbekannter Fehler')); } } catch (error) { console.error('Push error:', error); showError('Upload fehlgeschlagen'); } 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'; const card = document.createElement('div'); card.className = 'modalContent card'; card.style.maxWidth = '500px'; const title = document.createElement('h2'); title.textContent = '💬 Commit-Nachricht'; const inputGroup = document.createElement('div'); inputGroup.className = 'input-group'; const label = document.createElement('label'); label.textContent = 'Was wurde geändert?'; const input = document.createElement('input'); input.id = 'commitMsgInput'; input.type = 'text'; input.placeholder = 'z.B. Fix: Button-Farbe angepasst'; input.style.fontSize = '15px'; input.autocomplete = 'off'; inputGroup.appendChild(label); inputGroup.appendChild(input); const quickBtns = document.createElement('div'); quickBtns.id = 'commitQuickBtns'; quickBtns.style.cssText = 'margin-top: 8px; display: flex; flex-wrap: wrap; gap: 8px;'; ['🐛 Fix Bug', '✨ Neues Feature', '📝 Dokumentation', '♻️ Refactoring', '🚀 Release'].forEach((text) => { const btn = document.createElement('button'); btn.className = 'commit-quick-btn'; btn.style.cssText = ` background: var(--bg-tertiary); border: 1px solid rgba(255,255,255,0.1); color: var(--text-secondary); padding: 6px 12px; border-radius: 20px; cursor: pointer; font-size: 12px; `; btn.textContent = text; quickBtns.appendChild(btn); }); const modalButtons = document.createElement('div'); modalButtons.className = 'modal-buttons'; modalButtons.style.marginTop = '20px'; const okButton = document.createElement('button'); okButton.id = 'btnCommitOk'; okButton.className = 'accent-btn'; okButton.textContent = '⬆️ Pushen'; const cancelButton = document.createElement('button'); cancelButton.id = 'btnCommitCancel'; cancelButton.className = 'secondary'; cancelButton.textContent = 'Abbrechen'; modalButtons.appendChild(okButton); modalButtons.appendChild(cancelButton); card.appendChild(title); card.appendChild(inputGroup); card.appendChild(quickBtns); card.appendChild(modalButtons); modal.appendChild(card); document.body.appendChild(modal); 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(); }); }); } function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function showSyncConfirmModal({ title = 'Bestaetigen', message = '', confirmText = 'Fortfahren', details = [] }) { return new Promise((resolve) => { const modal = document.createElement('div'); modal.className = 'modal'; const card = document.createElement('div'); card.className = 'card'; card.style.width = 'min(520px, 92vw)'; const titleEl = document.createElement('h2'); titleEl.style.marginBottom = '8px'; titleEl.textContent = String(title || 'Bestaetigen'); const messageEl = document.createElement('p'); messageEl.style.cssText = 'margin: 0; color: var(--text-secondary); line-height: 1.5;'; messageEl.textContent = String(message || ''); card.appendChild(titleEl); card.appendChild(messageEl); if (Array.isArray(details)) { const rows = details.filter(d => d && d.label); if (rows.length > 0) { const detailBlock = document.createElement('div'); detailBlock.style.cssText = 'margin-top:12px;padding:10px 12px;border-radius:10px;border:1px solid rgba(255,255,255,0.08);background:linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));'; const detailTitle = document.createElement('div'); detailTitle.style.cssText = 'font-size:12px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;color:#b9c9e8;margin-bottom:4px;'; detailTitle.textContent = 'Was wird uebernommen'; detailBlock.appendChild(detailTitle); rows.forEach((d) => { const row = document.createElement('div'); row.style.cssText = 'display:flex;justify-content:space-between;gap:10px;padding:6px 0;border-bottom:1px dashed rgba(255,255,255,0.08);'; const left = document.createElement('span'); left.style.cssText = 'color:var(--text-secondary);font-size:12px;'; left.textContent = String(d.label || ''); const right = document.createElement('strong'); right.style.cssText = 'color:var(--text-primary);font-size:12px;text-align:right;word-break:break-word;'; right.textContent = String(d.value || ''); row.appendChild(left); row.appendChild(right); detailBlock.appendChild(row); }); card.appendChild(detailBlock); } } const btnRow = document.createElement('div'); btnRow.className = 'modal-buttons'; btnRow.style.marginTop = '18px'; const confirmBtn = document.createElement('button'); confirmBtn.id = 'btnSyncConfirm'; confirmBtn.className = 'accent-btn'; confirmBtn.textContent = String(confirmText || 'Fortfahren'); const cancelBtn = document.createElement('button'); cancelBtn.id = 'btnSyncCancel'; cancelBtn.className = 'secondary'; cancelBtn.textContent = 'Abbrechen'; btnRow.appendChild(confirmBtn); btnRow.appendChild(cancelBtn); card.appendChild(btnRow); modal.appendChild(card); const close = (result) => { modal.remove(); resolve(!!result); }; document.body.appendChild(modal); modal.querySelector('#btnSyncConfirm').addEventListener('click', () => close(true)); modal.querySelector('#btnSyncCancel').addEventListener('click', () => close(false)); modal.addEventListener('click', (e) => { if (e.target === modal) close(false); }); }); } 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, isPrivate = false, repoMeta = null) { 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.textContent = `${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(); const writable = isRepoWritable(repoMeta || {}, currentGiteaUsername); // ── Projektbild ändern ── if (writable && currentState.platform === 'gitea') { menu.appendChild(createMenuItem('🖼️', 'Projektbild ändern', () => { menu.remove(); const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; fileInput.style.display = 'none'; document.body.appendChild(fileInput); fileInput.onchange = async (e) => { const file = e.target.files[0]; fileInput.remove(); if (!file) return; const reader = new FileReader(); reader.onload = async (ev) => { const creds = await window.electronAPI.loadCredentials(); if (!creds?.giteaToken || !creds?.giteaURL) { showError('Gitea Token oder URL fehlt. Bitte zuerst in den Einstellungen speichern.'); return; } showProgress(30, `Bild für "${repoName}" hochladen…`); const result = await window.electronAPI.updateGiteaRepoAvatar({ token: creds.giteaToken, url: creds.giteaURL, owner, repo: repoName, imageBase64: ev.target.result }); hideProgress(); if (result?.ok) { showSuccess(`Projektbild für "${repoName}" aktualisiert`); loadGiteaRepos(); } else { showError('Upload fehlgeschlagen: ' + (result?.error || 'Unbekannter Fehler')); } }; reader.readAsDataURL(file); }; fileInput.click(); })); } const fullName = `${owner || ''}/${repoName || ''}`; const currentTopics = repoTopicsByFullName[fullName] || []; if (writable) { menu.appendChild(createMenuItem('🏷️', 'Tags bearbeiten', async () => { menu.remove(); await editRepoTopics(owner, repoName, currentTopics); })); } 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'); })); const isOwnRepo = String(owner || '').toLowerCase() === String(currentGiteaUsername || '').toLowerCase(); if (isOwnRepo) { const isGiteaView = currentState.platform === 'gitea'; const syncLabel = isGiteaView ? 'Gitea -> GitHub synchronisieren' : 'GitHub -> Gitea synchronisieren'; const syncIcon = isGiteaView ? '🔄' : '🔁'; menu.appendChild(createMenuItem(syncIcon, syncLabel, async () => { menu.remove(); if (isGiteaView) { const ok = await showSyncConfirmModal({ title: 'Sync bestaetigen', message: `Projekt "${repoName}" von Gitea nach GitHub synchronisieren?`, confirmText: 'Jetzt synchronisieren', details: [ { label: 'Richtung', value: 'Gitea -> GitHub' }, { label: 'Repository', value: `${owner}/${repoName}` }, { label: 'Sichtbarkeit', value: isPrivate ? 'Privat' : 'Oeffentlich' }, { label: 'Beschreibung', value: (repoMeta?.description || '-').slice(0, 160) }, { label: 'Topics', value: (Array.isArray(repoMeta?.topics) && repoMeta.topics.length > 0) ? repoMeta.topics.join(', ') : '-' } ] }); if (!ok) return; showProgress(20, `Synchronisiere ${owner}/${repoName} nach GitHub...`); const res = await window.electronAPI.syncRepoToGitHub({ owner, repo: repoName, cloneUrl, isPrivate, description: repoMeta?.description || '', homepage: repoMeta?.website || repoMeta?.homepage || '', topics: Array.isArray(repoMeta?.topics) ? repoMeta.topics : [] }); hideProgress(); if (res?.ok) { const createdText = res.repoCreated ? ' (neu erstellt)' : ' (bereits vorhanden)'; showSuccess(`Sync erfolgreich: ${res.githubRepo}${createdText}`); setStatus(`GitHub Sync OK: ${res.githubRepo}`); } else { showError('GitHub Sync fehlgeschlagen: ' + (res?.error || 'Unbekannter Fehler')); } } else { const ok = await showSyncConfirmModal({ title: 'Sync bestaetigen', message: `Projekt "${repoName}" von GitHub nach Gitea synchronisieren?`, confirmText: 'Jetzt synchronisieren', details: [ { label: 'Richtung', value: 'GitHub -> Gitea' }, { label: 'Repository', value: `${owner}/${repoName}` }, { label: 'Sichtbarkeit', value: isPrivate ? 'Privat' : 'Oeffentlich' }, { label: 'Beschreibung', value: (repoMeta?.description || '-').slice(0, 160) }, { label: 'Topics', value: (Array.isArray(repoMeta?.topics) && repoMeta.topics.length > 0) ? repoMeta.topics.join(', ') : '-' } ] }); if (!ok) return; const creds = await window.electronAPI.loadCredentials(); if (!creds?.giteaToken || !creds?.giteaURL) { showError('Gitea Token oder URL fehlt. Bitte in den Einstellungen speichern.'); return; } showProgress(20, `Synchronisiere ${owner}/${repoName} nach Gitea...`); const res = await window.electronAPI.migrateRepoToGitea({ cloneUrl, repoName, description: repoMeta?.description || `Synced from GitHub ${owner}/${repoName}`, isPrivate: !!isPrivate, authToken: creds.githubToken || '', authUsername: 'x-access-token' }); hideProgress(); if (res?.ok) { showSuccess(`Sync erfolgreich: Gitea/${repoName}`); setStatus(`Gitea Sync OK: ${repoName}`); } else { showError('Gitea Sync fehlgeschlagen: ' + (res?.error || 'Unbekannter Fehler')); } } })); } 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'); })); if (writable) { menu.appendChild(createMenuItem( isPrivate ? '🌍' : '🔒', isPrivate ? 'Öffentlich machen' : 'Privat machen', async () => { menu.remove(); await toggleRepoVisibility(owner, repoName, isPrivate); } )); } 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(); const ok = await showActionConfirmModal({ title: 'Repository loeschen', message: `Delete ${repoName}?`, confirmText: 'Loeschen', danger: true }); if (ok) { try { const res = await window.electronAPI.deleteGiteaRepo({ owner, repo: repoName, platform: currentState.platform }); 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'); if (writable) { 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.textContent = `${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 () => { const ok = await showActionConfirmModal({ title: 'Mehrfach-Loeschen', message: `${selectedItems.size} Elemente wirklich loeschen?`, confirmText: 'Loeschen', danger: true }); if (!ok) return; showProgress(0, 'Lösche...'); let done = 0; for (const p of selectedItems) { const isGithub = currentState.platform === 'github'; await window.electronAPI.deleteFile({ path: p, owner, repo, isGitea: !isGithub, isGithub, ref: getDefaultBranch(owner, repo) }); 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: currentState.platform !== 'github', isGithub: currentState.platform === 'github' }, 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 () => { const ok = await showActionConfirmModal({ title: 'Element loeschen', message: `"${item.name}" wirklich loeschen?`, confirmText: 'Loeschen', danger: true }); if (!ok) return; showProgress(0, `Lösche ${item.name}...`); const isGithub = currentState.platform === 'github'; const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: !isGithub, isGithub, ref: getDefaultBranch(owner, repo) }); hideProgress(); if (res && res.ok) { setStatus(`${item.name} gelöscht`); loadRepoContents(owner, repo, currentState.path); } else { showError('Löschen fehlgeschlagen: ' + (res?.error || '')); await showInfoModal('Loeschen fehlgeschlagen', 'Loeschen fehlgeschlagen:\n' + (res?.error || 'Unbekannter Fehler'), true); } }, '#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.textContent = `${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 () => { const ok = await showActionConfirmModal({ title: 'Mehrfach-Loeschen', message: `${selectedItems.size} Elemente wirklich loeschen?`, confirmText: 'Loeschen', danger: true }); if (!ok) 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 () => { const ok = await showActionConfirmModal({ title: 'Element loeschen', message: `"${node.name}" wirklich loeschen?`, confirmText: 'Loeschen', danger: true }); if (!ok) 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 { await showInfoModal('Umbenennen fehlgeschlagen', 'Umbenennen fehlgeschlagen:\n' + (res?.error || 'Unbekannter Fehler'), true); 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 { await showInfoModal('Erstellen fehlgeschlagen', 'Erstellen fehlgeschlagen:\n' + (res?.error || ''), true); } } }); } // 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 { await showInfoModal('Umbenennen fehlgeschlagen', 'Umbenennen fehlgeschlagen:\n' + (res?.error || ''), true); } } }); } // 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 { await showInfoModal('Erstellen fehlgeschlagen', 'Erstellen fehlgeschlagen:\n' + (res?.error || ''), true); } } }); } // 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 { await showInfoModal('Verschieben fehlgeschlagen', 'Verschieben fehlgeschlagen:\n' + (res?.error || ''), true); 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 { await showInfoModal('Verschieben fehlgeschlagen', 'Verschieben fehlgeschlagen:\n' + (res?.error || ''), true); } } // Generic Input Modal function showInputModal({ title, label, defaultValue, confirmText, onConfirm }) { const modal = document.createElement('div'); modal.className = 'modal'; modal.style.zIndex = '99999'; const card = document.createElement('div'); card.className = 'modalContent card'; card.style.maxWidth = '420px'; const titleEl = document.createElement('h2'); titleEl.textContent = String(title || ''); const group = document.createElement('div'); group.className = 'input-group'; const labelEl = document.createElement('label'); labelEl.textContent = String(label || ''); const inputEl = document.createElement('input'); inputEl.id = 'inputModalField'; inputEl.type = 'text'; inputEl.value = String(defaultValue || ''); inputEl.autocomplete = 'off'; group.appendChild(labelEl); group.appendChild(inputEl); const buttons = document.createElement('div'); buttons.className = 'modal-buttons'; buttons.style.marginTop = '16px'; const okBtn = document.createElement('button'); okBtn.id = 'inputModalOk'; okBtn.className = 'accent-btn'; okBtn.textContent = String(confirmText || 'Bestätigen'); const cancelBtn = document.createElement('button'); cancelBtn.id = 'inputModalCancel'; cancelBtn.className = 'secondary'; cancelBtn.textContent = 'Abbrechen'; buttons.appendChild(okBtn); buttons.appendChild(cancelBtn); card.appendChild(titleEl); card.appendChild(group); card.appendChild(buttons); modal.appendChild(card); 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(); }; } function showActionConfirmModal({ title, message, confirmText = 'Bestätigen', danger = false }) { return new Promise((resolve) => { const modal = document.createElement('div'); modal.className = 'modal confirm-modal'; modal.style.zIndex = '99999'; const card = document.createElement('div'); card.className = 'modalContent card confirm-modal-content'; const titleEl = document.createElement('h2'); titleEl.className = 'confirm-modal-title'; titleEl.textContent = `${danger ? '🗑️' : 'ℹ️'} ${title || 'Bestätigung'}`; const msgEl = document.createElement('p'); msgEl.className = 'confirm-modal-message'; msgEl.textContent = String(message || ''); const btnWrap = document.createElement('div'); btnWrap.className = 'modal-buttons confirm-modal-buttons'; const okBtn = document.createElement('button'); okBtn.id = 'actionConfirmOk'; okBtn.className = `confirm-modal-btn ${danger ? 'confirm-modal-btn--danger' : 'confirm-modal-btn--primary'}`; okBtn.textContent = String(confirmText || 'Bestätigen'); const cancelBtn = document.createElement('button'); cancelBtn.id = 'actionConfirmCancel'; cancelBtn.className = 'confirm-modal-btn confirm-modal-btn--secondary'; cancelBtn.textContent = 'Abbrechen'; btnWrap.appendChild(okBtn); btnWrap.appendChild(cancelBtn); card.appendChild(titleEl); card.appendChild(msgEl); card.appendChild(btnWrap); modal.appendChild(card); document.body.appendChild(modal); const okBtnNode = modal.querySelector('#actionConfirmOk'); const cancelBtnNode = modal.querySelector('#actionConfirmCancel'); if (okBtnNode) okBtnNode.focus(); const closeWith = (result) => { modal.remove(); resolve(result); }; if (okBtnNode) okBtnNode.onclick = () => closeWith(true); if (cancelBtnNode) cancelBtnNode.onclick = () => closeWith(false); modal.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeWith(false); if (e.key === 'Enter') closeWith(true); }); modal.onclick = (e) => { if (e.target === modal) closeWith(false); }; }); } function showInfoModal(title, message, danger = false) { return showActionConfirmModal({ title, message, confirmText: 'OK', danger }); } /* ------------------------- 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), platform: currentState.platform }); if (res.ok) { 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 = await showActionConfirmModal({ title: 'Aehnliche Namen gefunden', message: `Aehnliche Repository-Namen gefunden: ${check.similar.slice(0, 3).join(', ')}\n\nTrotzdem erstellen?`, confirmText: 'Trotzdem erstellen', danger: false }); 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.textContent = `${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 = extractDroppedPaths(files); if (paths.length === 0) { showError('Dateipfade konnten nicht gelesen werden (Sandbox/Drag-Quelle).'); return; } setStatus(`Uploading ${paths.length} items to /${targetPath || 'root'}...`); try { const res = await uploadDroppedPaths({ paths, owner, repo, destPath: targetPath, branch: getDefaultBranch(owner, repo) }); if (!res.ok) { console.error('Upload error:', res.error); showError('Error: ' + (res.error || 'Upload failed')); } } 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 => { try { window.electronAPI.debugToMain('log', 'document:drop', { currentView: currentState.view, fileCount: e.dataTransfer?.files ? e.dataTransfer.files.length : 0, itemCount: e.dataTransfer?.items ? e.dataTransfer.items.length : 0 }); } catch (_) {} if (currentState.view !== 'gitea-repo') { e.preventDefault(); } }); // Load credentials and auto-login if available try { const creds = await window.electronAPI.loadCredentials(); const credentialStatus = await window.electronAPI.getCredentialsStatus(); if (creds) { // Fülle Settings-Felder if ($('githubToken')) $('githubToken').value = creds.githubToken || ''; if ($('giteaToken')) $('giteaToken').value = creds.giteaToken || ''; if ($('giteaURL')) $('giteaURL').value = creds.giteaURL || ''; renderGithubTokenHint(creds.githubToken || ''); renderGiteaUrlHint(creds.giteaURL || '', creds.giteaToken || ''); // Avatar anzeigen if (creds.avatarB64) { const img = $('settingsAvatarImg'); const ph = $('settingsAvatarPlaceholder'); if (img) { img.src = creds.avatarB64; img.style.display = 'block'; } if (ph) ph.style.display = 'none'; } 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; // Standard beim App-Start: zuerst "Meine" Repositories anzeigen. activeRepoOwnerFilter = 'mine'; // AUTO-LOGIN: je nach ausgewaehlter Plattform nur mit passenden Credentials laden const selectedPlatform = ($('platform')?.value || currentState.platform || 'gitea'); const canAutoload = selectedPlatform === 'github' ? !!creds.githubToken : !!(creds.giteaToken && creds.giteaURL); if (canAutoload) { setStatus('Lade deine Projekte...'); setTimeout(() => { loadRepos(); }, 350); } else { setStatus('Bereit - bitte Settings konfigurieren'); } } else { setStatus('Bereit - bitte Settings konfigurieren'); renderGithubTokenHint(''); renderGiteaUrlHint('', ''); updateSettingsHealth({ url: 'Nicht konfiguriert', api: 'Nicht konfiguriert', auth: 'Kein Token', latency: '-', version: '-', lastError: '-' }); if (credentialStatus && credentialStatus.reason === 'safeStorage-decrypt-failed') { showError('Gespeicherte Zugangsdaten konnten nicht entschlüsselt werden. Bitte GitHub- und Gitea-Token neu eingeben und speichern.'); updateSettingsHealth({ auth: 'Nicht lesbar', lastError: 'Credentials nicht entschlüsselbar' }); } else if (credentialStatus && credentialStatus.reason === 'safeStorage-unavailable') { showError('Die aktuelle Sitzung kann die gespeicherten Zugangsdaten nicht über safeStorage lesen. Bitte erneut eingeben und speichern.'); updateSettingsHealth({ auth: 'Nicht lesbar', lastError: 'safeStorage nicht verfügbar' }); } } } 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 = loadRepos; } 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, $('giteaToken')?.value || ''); const checked = normalizeAndValidateGiteaUrl(raw); updateSettingsHealth({ url: checked.ok && checked.value ? 'Gültig' : (checked.ok ? 'Leer' : 'Ungültig') }); }); } if ($('giteaToken')) { $('giteaToken').addEventListener('input', (e) => { renderGiteaUrlHint($('giteaURL')?.value || '', e.target.value || ''); }); } if ($('githubToken')) { $('githubToken').addEventListener('input', (e) => { renderGithubTokenHint(e.target.value || ''); }); } 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('Gitea: Nicht verbunden'); 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('Gitea: Verbunden'); else showError('Gitea: Nicht verbunden'); } catch (error) { console.error('test-gitea-connection error:', error); showError('Gitea: Nicht verbunden'); } finally { btn.disabled = false; btn.textContent = oldText; } }; } if ($('btnTestGithubConnection')) { $('btnTestGithubConnection').onclick = async () => { const token = $('githubToken')?.value || ''; if (!token) { showError('GitHub: Nicht verbunden'); return; } setStatus('Teste GitHub-Verbindung...'); const btn = $('btnTestGithubConnection'); const oldText = btn.textContent; btn.disabled = true; btn.textContent = 'Teste...'; try { const res = await window.electronAPI.testGithubConnection({ token }); if (!res.ok) { showError('GitHub: Nicht verbunden'); return; } showSuccess('GitHub: Verbunden'); updateSettingsHealth({ auth: 'OK', lastError: '-' }); } catch (error) { console.error('test-github-connection error:', error); showError('GitHub: Nicht verbunden'); } 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'); }; } // ── Migration Modal ── if ($('btnOpenMigration')) { $('btnOpenMigration').onclick = () => { $('migrationModal').classList.remove('hidden'); }; } if ($('btnCloseMigration')) { $('btnCloseMigration').onclick = () => { $('migrationModal').classList.add('hidden'); $('migrationStatus').classList.add('hidden'); $('migrationStatus').textContent = ''; }; } if ($('migrateCloneUrl')) { $('migrateCloneUrl').addEventListener('input', () => { // Repo-Name aus URL automatisch ableiten const url = $('migrateCloneUrl').value.trim(); const match = url.match(/\/([^/]+?)(\.git)?$/); if (match && !$('migrateRepoName').value) { $('migrateRepoName').value = match[1]; } }); } if ($('btnStartMigration')) { $('btnStartMigration').onclick = async () => { const cloneUrl = $('migrateCloneUrl')?.value.trim(); const repoName = $('migrateRepoName')?.value.trim(); if (!cloneUrl) { showError('Bitte eine Quell-URL eingeben.'); return; } if (!repoName) { showError('Bitte einen Repository-Namen eingeben.'); return; } const statusEl = $('migrationStatus'); const btn = $('btnStartMigration'); btn.disabled = true; btn.textContent = '⏳ Migration läuft…'; statusEl.className = 'migration-status migration-status--running'; statusEl.textContent = `⏳ Migriere "${repoName}" von ${cloneUrl} …`; statusEl.classList.remove('hidden'); const result = await window.electronAPI.migrateRepoToGitea({ cloneUrl, repoName, description: $('migrateDescription')?.value.trim() || '', isPrivate: $('migratePrivate')?.checked || false, authUsername: $('migrateAuthUsername')?.value.trim() || '', authToken: $('migrateAuthToken')?.value.trim() || '' }); btn.disabled = false; btn.textContent = '📥 Migration starten'; if (result?.ok) { statusEl.className = 'migration-status migration-status--ok'; statusEl.textContent = `✅ "${repoName}" wurde erfolgreich migriert!`; showSuccess(`Repository "${repoName}" migriert`); // Felder leeren $('migrateCloneUrl').value = ''; $('migrateRepoName').value = ''; $('migrateDescription').value = ''; $('migrateAuthUsername').value = ''; $('migrateAuthToken').value = ''; $('migratePrivate').checked = false; loadGiteaRepos(); } else { statusEl.className = 'migration-status migration-status--error'; statusEl.textContent = `❌ Fehler: ${result?.error || 'Unbekannter Fehler'}`; showError('Migration fehlgeschlagen: ' + (result?.error || '')); } }; } 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 (_) {} // Avatar-Picker if ($('settingsAvatarWrap')) { $('settingsAvatarWrap').onclick = () => $('settingsAvatarInput')?.click(); } if ($('settingsAvatarInput')) { $('settingsAvatarInput').onchange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { const img = $('settingsAvatarImg'); const ph = $('settingsAvatarPlaceholder'); if (img) { img.src = ev.target.result; img.style.display = 'block'; } if (ph) ph.style.display = 'none'; }; reader.readAsDataURL(file); }; } // Avatar direkt auf Gitea hochladen if ($('btnUploadAvatar')) { $('btnUploadAvatar').onclick = async () => { const btn = $('btnUploadAvatar'); const img = $('settingsAvatarImg'); const avatarB64 = img && img.style.display !== 'none' ? img.src : null; if (!avatarB64 || !avatarB64.startsWith('data:')) { showError('Kein Profilbild ausgewählt. Bitte zuerst ein Bild auswählen.'); return; } const token = $('giteaToken')?.value || ''; const url = $('giteaURL')?.value || ''; if (!token || !url) { showError('Gitea Token und URL müssen eingetragen sein.'); return; } btn.disabled = true; btn.textContent = '⏳ Wird hochgeladen…'; try { const result = await window.electronAPI.updateGiteaAvatar({ token, url, imageBase64: avatarB64 }); if (result && result.ok) { btn.textContent = '✅ Aktualisiert'; setTimeout(() => { btn.textContent = '📤 Auf Gitea aktualisieren'; btn.disabled = false; }, 2500); } else { showError('Upload fehlgeschlagen: ' + (result?.error || 'Unbekannter Fehler')); btn.textContent = '📤 Auf Gitea aktualisieren'; btn.disabled = false; } } catch (err) { showError('Upload fehlgeschlagen: ' + err.message); btn.textContent = '📤 Auf Gitea aktualisieren'; btn.disabled = false; } }; } 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 credentialStatus = await window.electronAPI.getCredentialsStatus(); const githubTokenValue = $('githubToken').value; const giteaTokenValue = $('giteaToken').value; if ( credentialStatus && (credentialStatus.reason === 'safeStorage-decrypt-failed' || credentialStatus.reason === 'safeStorage-unavailable') && !String(githubTokenValue || '').trim() && !String(giteaTokenValue || '').trim() && !String(checkedUrl.value || '').trim() ) { showError('Gespeicherte Zugangsdaten sind nicht lesbar. Bitte Token und URL neu eingeben, bevor du speicherst.'); return; } const existingCreds = await window.electronAPI.loadCredentials() || {}; // aktuell ausgewählten Avatar-Base64 aus dem img-Element lesen const avatarImg = $('settingsAvatarImg'); const avatarB64 = (avatarImg && avatarImg.src && avatarImg.style.display !== 'none') ? avatarImg.src : (existingCreds.avatarB64 || null); const data = { ...existingCreds, githubToken: githubTokenValue, giteaToken: giteaTokenValue, giteaURL: checkedUrl.value, avatarB64, featureFavorites, featureRecent, compactMode, featureColoredIcons, favCollapsedFavorites: favSectionCollapsed.favorites, favCollapsedRecent: favSectionCollapsed.recent }; await window.electronAPI.saveCredentials(data); // Avatar automatisch zu Gitea pushen if (avatarB64 && avatarB64.startsWith('data:') && data.giteaToken && data.giteaURL) { const btnSave = $('btnSaveSettings'); if (btnSave) btnSave.textContent = '⏳ Bild wird hochgeladen…'; try { const avatarResult = await window.electronAPI.updateGiteaAvatar({ token: data.giteaToken, url: data.giteaURL, imageBase64: avatarB64 }); if (avatarResult && !avatarResult.ok) { console.error('Avatar-Upload Fehler:', avatarResult.error); showError('Profilbild konnte nicht hochgeladen werden: ' + (avatarResult.error || 'Unbekannter Fehler')); } } catch (avatarErr) { console.error('Avatar-Upload Exception:', avatarErr); showError('Profilbild-Upload fehlgeschlagen: ' + avatarErr.message); } finally { if (btnSave) btnSave.textContent = 'Speichern'; } } $('settingsModal').classList.add('hidden'); showSuccess('Settings saved'); renderGithubTokenHint(data.githubToken || ''); renderGiteaUrlHint(checkedUrl.value, data.giteaToken || ''); 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; currentReleaseView.platform = currentState.platform; setStatus('Loading releases...'); try { const res = await window.electronAPI.listReleases({ owner, repo, platform: currentState.platform }); if (!res.ok) { showError('Error loading releases: ' + res.error); return; } const grid = $('explorerGrid'); if (!grid) return; // Header mit "New Release" Button grid.innerHTML = ''; const releaseHeader = document.createElement('div'); releaseHeader.style.cssText = 'grid-column: 1/-1; display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;'; const releaseTitle = document.createElement('h2'); releaseTitle.style.cssText = 'margin: 0; color: var(--text-primary);'; releaseTitle.textContent = `📦 Releases für ${repo}`; const newBtn = document.createElement('button'); newBtn.className = 'btn-new-release'; newBtn.style.cssText = ` background: var(--accent-gradient); color: #000; border: none; padding: 10px 20px; border-radius: var(--radius-md); font-weight: 700; cursor: pointer; display: flex; align-items: center; gap: 8px; `; newBtn.textContent = '🚀 New Release'; newBtn.onclick = () => { console.log('New Release button clicked'); showCreateReleaseModal(owner, repo); }; releaseHeader.appendChild(releaseTitle); releaseHeader.appendChild(newBtn); grid.appendChild(releaseHeader); 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 () => { const ok = await showActionConfirmModal({ title: 'Asset loeschen', message: `Delete asset "${asset.name}"?`, confirmText: 'Loeschen', danger: true }); if (ok) { 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' }); const dateMeta = document.createElement('span'); dateMeta.textContent = `📅 ${dateStr}`; const authorMeta = document.createElement('span'); authorMeta.textContent = `👤 ${release.author?.login || 'Unknown'}`; meta.appendChild(dateMeta); meta.appendChild(authorMeta); 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 () => { const ok = await showActionConfirmModal({ title: 'Release loeschen', message: `Delete release "${release.name || release.tag_name}"?`, confirmText: 'Loeschen', danger: true }); if (ok) { const res = await window.electronAPI.deleteRelease({ owner: currentReleaseView.owner, repo: currentReleaseView.repo, releaseId: release.id, platform: currentReleaseView.platform || currentState.platform }); 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'; const card = document.createElement('div'); card.className = 'card'; card.style.cssText = 'width: 600px; max-width: 90vw;'; const title = document.createElement('h2'); title.textContent = '🚀 Neues Release erstellen'; card.appendChild(title); const tagGroup = document.createElement('div'); tagGroup.className = 'input-group'; const tagLabel = document.createElement('label'); tagLabel.textContent = 'Tag Version *'; const tagInput = document.createElement('input'); tagInput.id = 'releaseTag'; tagInput.type = 'text'; tagInput.placeholder = 'v1.0.0'; tagGroup.appendChild(tagLabel); tagGroup.appendChild(tagInput); card.appendChild(tagGroup); const nameGroup = document.createElement('div'); nameGroup.className = 'input-group'; const nameLabel = document.createElement('label'); nameLabel.textContent = 'Release Name'; const nameInput = document.createElement('input'); nameInput.id = 'releaseName'; nameInput.type = 'text'; nameInput.placeholder = 'Version 1.0.0'; nameGroup.appendChild(nameLabel); nameGroup.appendChild(nameInput); card.appendChild(nameGroup); const bodyGroup = document.createElement('div'); bodyGroup.className = 'input-group'; const bodyLabel = document.createElement('label'); bodyLabel.textContent = 'Release Notes'; const bodyInput = document.createElement('textarea'); bodyInput.id = 'releaseBody'; bodyInput.rows = 8; bodyInput.placeholder = '## Was ist neu?\n\n- Feature 1\n- Feature 2\n- Bug Fixes'; bodyInput.style.cssText = 'width: 100%; padding: 12px; border-radius: var(--radius-md); border: 1px solid rgba(255, 255, 255, 0.1); background: var(--bg-tertiary); color: var(--text-primary); font-family: monospace; resize: vertical;'; bodyGroup.appendChild(bodyLabel); bodyGroup.appendChild(bodyInput); card.appendChild(bodyGroup); const targetGroup = document.createElement('div'); targetGroup.className = 'input-group'; const targetLabel = document.createElement('label'); targetLabel.textContent = 'Target Branch'; const targetInput = document.createElement('input'); targetInput.id = 'releaseTarget'; targetInput.type = 'text'; targetInput.value = 'main'; targetInput.placeholder = 'main'; targetGroup.appendChild(targetLabel); targetGroup.appendChild(targetInput); card.appendChild(targetGroup); const assetGroup = document.createElement('div'); assetGroup.className = 'input-group'; const assetLabel = document.createElement('label'); assetLabel.textContent = 'Release Asset (Optional)'; const assetRow = document.createElement('div'); assetRow.style.cssText = 'display: flex; gap: 10px;'; const assetInput = document.createElement('input'); assetInput.id = 'releaseAssetInput'; assetInput.type = 'text'; assetInput.readOnly = true; assetInput.placeholder = 'Keine Datei gewählt'; assetInput.style.cssText = 'flex: 1; padding: 10px; border-radius: var(--radius-md); border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(0, 0, 0, 0.2); color: var(--text-muted); cursor: not-allowed;'; const assetBtn = document.createElement('button'); assetBtn.id = 'btnSelectReleaseAsset'; assetBtn.type = 'button'; assetBtn.textContent = '📎 Datei wählen'; assetBtn.style.cssText = 'padding: 10px 20px; border-radius: var(--radius-md); background: var(--bg-secondary); color: var(--text-primary); border: 1px solid rgba(255, 255, 255, 0.1); cursor: pointer; font-weight: 600;'; assetRow.appendChild(assetInput); assetRow.appendChild(assetBtn); assetGroup.appendChild(assetLabel); assetGroup.appendChild(assetRow); card.appendChild(assetGroup); const optionsGroup = document.createElement('div'); optionsGroup.className = 'input-group'; optionsGroup.style.cssText = 'display: flex; gap: 20px;'; const preLabel = document.createElement('label'); preLabel.style.cssText = 'display: flex; align-items: center; gap: 8px; cursor: pointer;'; const preCheck = document.createElement('input'); preCheck.type = 'checkbox'; preCheck.id = 'releasePrerelease'; preLabel.appendChild(preCheck); preLabel.appendChild(document.createTextNode(' Pre-Release')); const draftLabel = document.createElement('label'); draftLabel.style.cssText = 'display: flex; align-items: center; gap: 8px; cursor: pointer;'; const draftCheck = document.createElement('input'); draftCheck.type = 'checkbox'; draftCheck.id = 'releaseDraft'; draftLabel.appendChild(draftCheck); draftLabel.appendChild(document.createTextNode(' Draft (nicht veröffentlichen)')); optionsGroup.appendChild(preLabel); optionsGroup.appendChild(draftLabel); card.appendChild(optionsGroup); const modalButtons = document.createElement('div'); modalButtons.className = 'modal-buttons'; const createBtn = document.createElement('button'); createBtn.id = 'btnCreateRelease'; createBtn.textContent = 'Erstellen & Veröffentlichen'; const cancelBtn = document.createElement('button'); cancelBtn.id = 'btnCancelRelease'; cancelBtn.textContent = 'Abbrechen'; modalButtons.appendChild(createBtn); modalButtons.appendChild(cancelBtn); card.appendChild(modalButtons); modal.appendChild(card); 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); await showInfoModal('Dateidialog', 'Konnte Dateidialog nicht oeffnen.', true); } }; // 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) { await showInfoModal('Eingabe fehlt', '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, platform: currentState.platform }); 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); await showInfoModal('Asset Upload fehlgeschlagen', `Release erstellt, aber Asset Upload fehlgeschlagen: ${uploadRes.error || 'Unbekannter Fehler'}`, true); showWarning('Release erstellt (Upload Fehler)'); } } catch (uploadErr) { console.error('Upload error:', uploadErr); await showInfoModal('Upload fehlgeschlagen', 'Release erstellt, aber Fehler beim Hochladen der Datei.', true); showWarning('Release erstellt (Upload Fehler)'); } finally { hideProgress(); } } else { setStatus('Release created!'); } modal.remove(); loadRepoReleases(owner, repo); // Liste neu laden } else { showError('Failed: ' + res.error); await showInfoModal('Release erstellen fehlgeschlagen', 'Fehler beim Erstellen des Releases: ' + (res.error || 'Unbekannter Fehler'), true); } } catch (error) { console.error('Create release error:', error); showError('Create failed'); await showInfoModal('Unerwarteter Fehler', 'Ein unerwarteter Fehler ist aufgetreten.', true); } }; $('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; currentCommitView.platform = currentState.platform; setStatus('Loading commit history...'); try { const res = await window.electronAPI.getCommits({ owner, repo, branch, limit: 100, platform: currentState.platform }); 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; `; const title = document.createElement('h2'); title.style.cssText = 'margin: 0; color: var(--text-primary); display: flex; align-items: center; gap: 12px;'; title.textContent = '📊 Commit History'; const titleMeta = document.createElement('span'); titleMeta.style.cssText = 'font-size: 14px; color: var(--text-muted); font-weight: 400;'; titleMeta.textContent = `${currentCommitView.repo} / ${currentCommitView.branch}`; title.appendChild(titleMeta); const searchWrap = document.createElement('div'); searchWrap.style.cssText = 'display: flex; gap: 12px; flex: 1; max-width: 600px;'; const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.id = 'commitSearch'; searchInput.placeholder = '🔍 Search commits (message, author)...'; searchInput.style.cssText = ` flex: 1; padding: 10px 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; `; const clearBtn = document.createElement('button'); clearBtn.id = 'btnClearSearch'; clearBtn.textContent = 'Clear'; clearBtn.style.cssText = ` padding: 10px 16px; border-radius: var(--radius-md); background: var(--bg-secondary); color: var(--text-primary); border: 1px solid rgba(255, 255, 255, 0.1); cursor: pointer; font-weight: 600; `; searchWrap.appendChild(searchInput); searchWrap.appendChild(clearBtn); header.appendChild(title); header.appendChild(searchWrap); grid.appendChild(header); // Search-Handler 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) || '???????'; const authorMeta = document.createElement('span'); authorMeta.style.cssText = 'display: flex; align-items: center; gap: 6px;'; authorMeta.appendChild(document.createTextNode('👤 ')); const authorStrong = document.createElement('strong'); authorStrong.textContent = author; authorMeta.appendChild(authorStrong); const dateMeta = document.createElement('span'); dateMeta.style.cssText = 'display: flex; align-items: center; gap: 6px;'; dateMeta.textContent = `🕐 ${dateStr}`; const shaMeta = document.createElement('span'); shaMeta.style.cssText = 'display: flex; align-items: center; gap: 6px; font-family: monospace; background: rgba(255,255,255,0.05); padding: 2px 8px; border-radius: 4px;'; shaMeta.textContent = `#${sha}`; meta.appendChild(authorMeta); meta.appendChild(dateMeta); meta.appendChild(shaMeta); 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); `; const addStat = document.createElement('span'); addStat.style.color = 'var(--success)'; addStat.textContent = `+${commit.stats.additions || 0}`; const delStat = document.createElement('span'); delStat.style.color = 'var(--danger)'; delStat.textContent = `-${commit.stats.deletions || 0}`; const totalStat = document.createElement('span'); totalStat.style.color = 'var(--text-muted)'; totalStat.textContent = `${commit.stats.total || 0} changes`; stats.appendChild(addStat); stats.appendChild(delStat); stats.appendChild(totalStat); 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'; const card = document.createElement('div'); card.className = 'card'; card.style.cssText = 'width: 90vw; max-width: 1200px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column;'; const header = document.createElement('div'); header.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;'; const title = document.createElement('h2'); title.style.margin = '0'; title.textContent = '📋 Commit Details'; const closeBtn = document.createElement('button'); closeBtn.id = 'btnCloseCommitModal'; closeBtn.style.cssText = 'background: transparent; border: none; font-size: 24px; cursor: pointer; color: var(--text-muted);'; closeBtn.textContent = '✕'; header.appendChild(title); header.appendChild(closeBtn); const scroller = document.createElement('div'); scroller.style.cssText = 'overflow-y: auto; flex: 1;'; const detailsContent = document.createElement('div'); detailsContent.id = 'commitDetailsContent'; detailsContent.style.paddingBottom = '20px'; const loading = document.createElement('div'); loading.style.cssText = 'text-align: center; padding: 40px; color: var(--text-muted);'; loading.textContent = 'Loading commit details...'; detailsContent.appendChild(loading); scroller.appendChild(detailsContent); card.appendChild(header); card.appendChild(scroller); modal.appendChild(card); 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 { // Remote repository (Gitea or GitHub) const [diff, files] = await Promise.all([ window.electronAPI.getCommitDiff({ owner: currentCommitView.owner, repo: currentCommitView.repo, sha: commit.sha, platform: currentCommitView.platform || currentState.platform }), window.electronAPI.getCommitFiles({ owner: currentCommitView.owner, repo: currentCommitView.repo, sha: commit.sha, platform: currentCommitView.platform || currentState.platform }) ]); 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 || ''; const titleEl = document.createElement('h3'); titleEl.style.cssText = 'margin: 0 0 16px 0; font-size: 20px; line-height: 1.4;'; titleEl.textContent = message; const metaRow = document.createElement('div'); metaRow.style.cssText = 'display: flex; gap: 24px; font-size: 14px; color: var(--text-muted); flex-wrap: wrap;'; const authorEl = document.createElement('span'); authorEl.appendChild(document.createTextNode('👤 ')); const authorStrong = document.createElement('strong'); authorStrong.textContent = author; authorEl.appendChild(authorStrong); if (email) { authorEl.appendChild(document.createTextNode(` <${email}>`)); } const dateEl = document.createElement('span'); dateEl.textContent = `🕐 ${date.toLocaleString()}`; const shaEl = document.createElement('span'); shaEl.style.cssText = 'font-family: monospace; background: rgba(255,255,255,0.05); padding: 4px 12px; border-radius: 6px;'; shaEl.textContent = sha.substring(0, 7); metaRow.appendChild(authorEl); metaRow.appendChild(dateEl); metaRow.appendChild(shaEl); header.appendChild(titleEl); header.appendChild(metaRow); 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; `; const filesTitle = document.createElement('h4'); filesTitle.style.cssText = 'margin: 0; display: flex; align-items: center; gap: 8px;'; filesTitle.textContent = `📁 Changed Files (${filesRes.files.length})`; const filesStats = document.createElement('div'); filesStats.style.cssText = 'display: flex; gap: 16px; font-size: 13px;'; const additionsEl = document.createElement('span'); additionsEl.style.color = 'var(--success)'; additionsEl.textContent = `+${filesRes.stats?.additions || 0}`; const deletionsEl = document.createElement('span'); deletionsEl.style.color = 'var(--danger)'; deletionsEl.textContent = `-${filesRes.stats?.deletions || 0}`; filesStats.appendChild(additionsEl); filesStats.appendChild(deletionsEl); filesHeader.appendChild(filesTitle); filesHeader.appendChild(filesStats); 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' ? '🗑️' : '📝'; const fileNameEl = document.createElement('span'); fileNameEl.style.cssText = 'font-family: monospace; color: var(--text-primary);'; fileNameEl.textContent = `${icon} ${String(file.file || '')}`; const fileStatsEl = document.createElement('span'); fileStatsEl.style.color = 'var(--text-muted)'; const fileAddEl = document.createElement('span'); fileAddEl.style.color = 'var(--success)'; fileAddEl.textContent = `+${file.insertions}`; const fileDelEl = document.createElement('span'); fileDelEl.style.cssText = 'color: var(--danger); margin-left: 8px;'; fileDelEl.textContent = `-${file.deletions}`; fileStatsEl.appendChild(fileAddEl); fileStatsEl.appendChild(fileDelEl); fileItem.appendChild(fileNameEl); fileItem.appendChild(fileStatsEl); 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 = ''; const errorWrap = document.createElement('div'); errorWrap.style.cssText = 'text-align: center; padding: 40px; color: var(--danger);'; const errorTitle = document.createElement('p'); errorTitle.textContent = '❌ Error loading commit details'; const errorDetails = document.createElement('p'); errorDetails.style.cssText = 'font-size: 12px; color: var(--text-muted); margin-top: 12px;'; errorDetails.textContent = `Source: ${isLocalGit} | Error: ${errorMsg}`; errorWrap.appendChild(errorTitle); errorWrap.appendChild(errorDetails); container.appendChild(errorWrap); } } 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(), platform: currentCommitView.platform || currentState.platform }); 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) { return String(text ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /* ------------------------- 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({ silent: false }); 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'); }; } }); } if (window.electronAPI.onUpdateNotAvailable) { window.electronAPI.onUpdateNotAvailable(() => { showInfo('Du nutzt bereits die aktuelle Version.'); }); } // 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 // Etwas spaeter, damit initiales UI/Repo-Laden nicht ausgebremst wird. setTimeout(() => { window.electronAPI.checkForUpdates({ silent: true }); }, 12000); });