Files
Git-Manager-Gui/renderer/renderer.js
2026-02-02 20:00:57 +01:00

3084 lines
99 KiB
JavaScript

// renderer.js — Grid-UI + Navigation + Drag'n'Drop mit Fehlerbehandlung
const $ = id => document.getElementById(id);
let selectedFolder = null;
let giteaCache = {};
// Navigations-Status für die Explorer-Ansicht
let currentState = {
view: 'none', // 'local', 'gitea-list', 'gitea-repo'
owner: null,
repo: null,
path: ''
};
function setStatus(txt) {
const s = $('status');
if (s) s.innerText = txt || '';
}
/* -------------------------
PROGRESS UI
------------------------- */
function ensureProgressUI() {
if ($('folderProgressContainer')) return;
const container = document.createElement('div');
container.id = 'folderProgressContainer';
container.style.cssText = `
position: fixed;
left: 50%;
top: 12px;
transform: translateX(-50%);
z-index: 10000;
width: 480px;
max-width: 90%;
padding: 12px 16px;
background: rgba(20,20,30,0.98);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
color: #fff;
font-family: sans-serif;
display: none;
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 212, 255, 0.2);
`;
const text = document.createElement('div');
text.id = 'folderProgressText';
text.style.cssText = `
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
color: #00d4ff;
`;
container.appendChild(text);
const barWrap = document.createElement('div');
barWrap.style.cssText = `
width: 100%;
height: 12px;
background: rgba(255,255,255,0.1);
border-radius: 6px;
overflow: hidden;
position: relative;
`;
const bar = document.createElement('div');
bar.id = 'folderProgressBar';
bar.style.cssText = `
width: 0%;
height: 100%;
background: linear-gradient(90deg, #00d4ff, #8b5cf6);
transition: width 200ms ease-out;
border-radius: 6px;
`;
barWrap.appendChild(bar);
container.appendChild(barWrap);
document.body.appendChild(container);
}
function showProgress(percent, text) {
ensureProgressUI();
const container = $('folderProgressContainer');
const bar = $('folderProgressBar');
const txt = $('folderProgressText');
if (txt) txt.innerText = text || '';
if (bar) bar.style.width = `${Math.min(100, Math.max(0, percent))}%`;
if (container) container.style.display = 'block';
}
function hideProgress() {
const container = $('folderProgressContainer');
if (container) {
setTimeout(() => {
container.style.display = 'none';
}, 500);
}
}
/* -------------------------
ADVANCED FILE EDITOR - WITH TABS, UNDO/REDO, AUTO-SAVE, LINE NUMBERS
------------------------- */
// Editor State
let openTabs = {}; // { filePath: { name, content, originalContent, dirty, icon, history, historyIndex } }
let currentActiveTab = null;
let autoSaveTimer = null;
let autoSaveInterval = 3000; // 3 sekunden
// Initialize editor
function initEditor() {
const textarea = $('fileEditorContent');
if (!textarea) return;
textarea.addEventListener('input', () => {
updateCurrentTab();
updateLineNumbers();
updateEditorStats();
triggerAutoSave();
});
textarea.addEventListener('scroll', () => {
const lineNumbers = $('lineNumbers');
if (lineNumbers) lineNumbers.scrollTop = textarea.scrollTop;
});
textarea.addEventListener('keydown', (e) => {
// Ctrl+Z - Undo
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
e.preventDefault();
undoChange();
}
// Ctrl+Shift+Z or Ctrl+Y - Redo
if (((e.ctrlKey || e.metaKey) && e.key === 'z' && e.shiftKey) ||
((e.ctrlKey || e.metaKey) && e.key === 'y')) {
e.preventDefault();
redoChange();
}
// Tab insertion
if (e.key === 'Tab') {
e.preventDefault();
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
textarea.value = textarea.value.substring(0, start) + '\t' + textarea.value.substring(end);
textarea.selectionStart = textarea.selectionEnd = start + 1;
updateCurrentTab();
updateLineNumbers();
}
});
}
// Add new tab
function addTab(filePath, fileName, content, isGitea = false, owner = null, repo = null) {
openTabs[filePath] = {
name: fileName,
content: content,
originalContent: content,
dirty: false,
icon: getFileIcon(fileName),
isGitea,
owner,
repo,
history: [content],
historyIndex: 0
};
currentActiveTab = filePath;
renderTabs();
updateEditor(); // Kann async sein
}
// Remove tab
function removeTab(filePath) {
delete openTabs[filePath];
if (currentActiveTab === filePath) {
const paths = Object.keys(openTabs);
currentActiveTab = paths.length > 0 ? paths[0] : null;
}
if (Object.keys(openTabs).length === 0) {
closeFileEditor();
} else {
renderTabs();
updateEditor();
}
}
// Switch tab
function switchTab(filePath) {
if (openTabs[filePath]) {
currentActiveTab = filePath;
renderTabs();
updateEditor(); // Kann async sein, aber wir warten nicht
}
}
// Render tabs
function renderTabs() {
const tabsContainer = $('fileEditorTabs');
if (!tabsContainer) return;
tabsContainer.innerHTML = '';
Object.entries(openTabs).forEach(([filePath, tab]) => {
const tabEl = document.createElement('div');
tabEl.className = `editor-tab ${currentActiveTab === filePath ? 'active' : ''}`;
const nameEl = document.createElement('div');
nameEl.className = 'editor-tab-name';
const iconEl = document.createElement('span');
iconEl.textContent = tab.icon;
const nameSpan = document.createElement('span');
nameSpan.textContent = tab.name;
if (tab.dirty) {
const dirtyEl = document.createElement('span');
dirtyEl.className = 'editor-tab-dirty';
dirtyEl.textContent = '●';
nameEl.appendChild(iconEl);
nameEl.appendChild(nameSpan);
nameEl.appendChild(dirtyEl);
} else {
nameEl.appendChild(iconEl);
nameEl.appendChild(nameSpan);
}
const closeBtn = document.createElement('button');
closeBtn.className = 'editor-tab-close';
closeBtn.textContent = '✕';
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (tab.dirty && !confirm(`${tab.name} hat ungespeicherte Änderungen. Wirklich schließen?`)) {
return;
}
removeTab(filePath);
});
tabEl.appendChild(nameEl);
tabEl.appendChild(closeBtn);
tabEl.addEventListener('click', () => switchTab(filePath));
tabsContainer.appendChild(tabEl);
});
}
// Update current tab content
function updateCurrentTab() {
if (!currentActiveTab) return;
const textarea = $('fileEditorContent');
const content = textarea.value;
const tab = openTabs[currentActiveTab];
if (!tab) return;
tab.content = content;
tab.dirty = (content !== tab.originalContent);
renderTabs();
}
// Update editor display
async function updateEditor() {
if (!currentActiveTab || !openTabs[currentActiveTab]) return;
const tab = openTabs[currentActiveTab];
const textarea = $('fileEditorContent');
const imagePreview = $('imagePreview');
// Prüfe ob es eine Bilddatei ist
const isImage = /\.(png|jpg|jpeg|gif|webp|svg)$/i.test(currentActiveTab);
if (isImage) {
// Zeige Bild statt Textarea
if (textarea) textarea.classList.add('hidden');
if (imagePreview) {
imagePreview.classList.remove('hidden');
let imgSrc = '';
if (tab.isGitea) {
// Gitea-Bild: Lade via API
try {
const filePath = currentActiveTab.replace(`gitea://${tab.owner}/${tab.repo}/`, '');
const response = await window.electronAPI.readGiteaFile({
owner: tab.owner,
repo: tab.repo,
path: filePath,
ref: 'main'
});
if (response.ok) {
// Content ist Base64 Text, konvertiere zu Data URL
const imageData = response.content;
const ext = filePath.split('.').pop().toLowerCase();
const mimeType = {
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'webp': 'image/webp',
'svg': 'image/svg+xml'
}[ext] || 'image/png';
imgSrc = `data:${mimeType};base64,${imageData}`;
}
} catch (error) {
console.error('Error loading Gitea image:', error);
imagePreview.innerHTML = '<div style="color: var(--text-muted); text-align: center;">Fehler beim Laden des Bildes</div>';
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 = '<div style="color: var(--text-muted); text-align: center; padding: 40px;">Bild konnte nicht geladen werden</div>';
};
// Container für zentrierte Anzeige
imagePreview.innerHTML = '';
imagePreview.style.cssText = `
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
overflow: auto;
padding: 20px;
`;
imagePreview.appendChild(img);
}
} else {
// Zeige Text Editor
if (textarea) {
textarea.classList.remove('hidden');
textarea.value = tab.content;
updateLineNumbers();
updateEditorStats();
}
if (imagePreview) imagePreview.classList.add('hidden');
}
updateTabInfo();
}
// Update tab info header
function updateTabInfo() {
const tab = openTabs[currentActiveTab];
if (!tab) return;
$('fileEditorName').textContent = tab.name;
$('fileEditorIcon').textContent = tab.icon;
const pathText = tab.isGitea ? `Gitea: ${tab.owner}/${tab.repo}/${currentActiveTab}` : `Pfad: ${currentActiveTab}`;
$('fileEditorPath').textContent = pathText;
const lines = tab.content.split('\n').length;
const bytes = new Blob([tab.content]).size;
$('fileEditorStats').textContent = `${lines} Zeilen • ${bytes} Bytes`;
}
// Update line numbers
function updateLineNumbers() {
const textarea = $('fileEditorContent');
const lineNumbers = $('lineNumbers');
if (!textarea || !lineNumbers) return;
const lines = textarea.value.split('\n').length;
let html = '';
for (let i = 1; i <= lines; i++) {
html += i + '\n';
}
lineNumbers.textContent = html;
lineNumbers.scrollTop = textarea.scrollTop;
}
// Update editor stats (cursor position)
function updateEditorStats() {
const textarea = $('fileEditorContent');
if (!textarea) return;
const lines = textarea.value.split('\n').length;
const startPos = textarea.selectionStart;
const textBeforeCursor = textarea.value.substring(0, startPos);
const line = textBeforeCursor.split('\n').length;
const col = startPos - textBeforeCursor.lastIndexOf('\n');
$('fileEditorCursor').textContent = `Zeile ${line}, Spalte ${col}`;
}
// Undo
function undoChange() {
if (!currentActiveTab) return;
const tab = openTabs[currentActiveTab];
if (tab.historyIndex > 0) {
tab.historyIndex--;
const textarea = $('fileEditorContent');
textarea.value = tab.history[tab.historyIndex];
updateCurrentTab();
updateLineNumbers();
updateEditorStats();
}
}
// Redo
function redoChange() {
if (!currentActiveTab) return;
const tab = openTabs[currentActiveTab];
if (tab.historyIndex < tab.history.length - 1) {
tab.historyIndex++;
const textarea = $('fileEditorContent');
textarea.value = tab.history[tab.historyIndex];
updateCurrentTab();
updateLineNumbers();
updateEditorStats();
}
}
// Push to history
function pushToHistory(content) {
if (!currentActiveTab) return;
const tab = openTabs[currentActiveTab];
// Remove any redo history
tab.history = tab.history.slice(0, tab.historyIndex + 1);
tab.history.push(content);
tab.historyIndex++;
// Limit history to 50 items
if (tab.history.length > 50) {
tab.history.shift();
tab.historyIndex--;
}
}
// Auto-Save
function triggerAutoSave() {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(() => {
saveCurrentFile(true);
}, autoSaveInterval);
}
function showAutoSaveIndicator() {
const indicator = $('autoSaveStatus');
if (indicator) {
indicator.style.display = 'inline';
setTimeout(() => {
indicator.style.display = 'none';
}, 2000);
}
}
function closeFileEditor() {
// Überprüfe auf ungespeicherte Änderungen
const unsaved = Object.entries(openTabs).filter(([_, tab]) => tab.dirty);
if (unsaved.length > 0) {
if (!confirm(`${unsaved.length} Datei(en) haben ungespeicherte Änderungen. Wirklich schließen?`)) {
return;
}
}
openTabs = {};
currentActiveTab = null;
clearTimeout(autoSaveTimer);
const modal = $('fileEditorModal');
if (modal) modal.classList.add('hidden');
}
async function openFileEditor(filePath, fileName) {
try {
console.log('🔍 Opening file:', filePath);
// Wenn bereits offen, nur switchen
if (openTabs[filePath]) {
switchTab(filePath);
const modal = $('fileEditorModal');
if (modal) modal.classList.remove('hidden');
return;
}
// Prüfe ob es eine Bilddatei ist
const isImage = /\.(png|jpg|jpeg|gif|webp|svg)$/i.test(fileName);
if (isImage) {
// Für Bilder brauchen wir keinen Content zu lesen
addTab(filePath, fileName, '', false, null, null);
} else {
// Lese Text-Datei
const response = await window.electronAPI.readFile({ path: filePath });
if (response.ok) {
addTab(filePath, fileName, response.content);
} else {
alert(`Fehler: ${response.error}`);
return;
}
}
const modal = $('fileEditorModal');
if (modal) {
modal.classList.remove('hidden');
initEditor();
$('fileEditorContent').focus();
}
setStatus(`Editiere: ${fileName}`);
console.log('✅ File opened');
} catch (error) {
console.error('Error opening file:', error);
alert('Fehler beim Öffnen der Datei');
}
}
async function openGiteaFileInEditor(owner, repo, filePath, fileName) {
try {
console.log('🔍 Loading Gitea file:', owner, repo, filePath);
setStatus('Lädt Datei...');
// Wenn bereits offen, nur switchen
const vPath = `gitea://${owner}/${repo}/${filePath}`;
if (openTabs[vPath]) {
switchTab(vPath);
const modal = $('fileEditorModal');
if (modal) modal.classList.remove('hidden');
return;
}
// Lade Datei-Content vom Gitea Handler
const response = await window.electronAPI.readGiteaFile({
owner,
repo,
path: filePath,
ref: 'main'
});
if (response.ok) {
addTab(vPath, fileName, response.content, true, owner, repo);
const modal = $('fileEditorModal');
if (modal) {
modal.classList.remove('hidden');
initEditor();
$('fileEditorContent').focus();
}
setStatus(`Editiere: ${fileName}`);
console.log('✅ Gitea file opened');
} else {
alert(`Fehler: ${response.error}`);
setStatus('Fehler beim Laden der Datei');
}
} catch (error) {
console.error('Error opening Gitea file:', error);
alert('Fehler beim Öffnen der Datei');
setStatus('Fehler');
}
}
function getFileIcon(fileName) {
const ext = fileName.split('.').pop()?.toLowerCase();
const icons = {
'js': '🟨', 'jsx': '⚛️', 'ts': '🔵', 'tsx': '⚛️',
'py': '🐍', 'java': '☕', 'cpp': '⚙️', 'c': '⚙️',
'html': '🌐', 'css': '🎨', 'scss': '🎨', 'json': '📋',
'md': '📝', 'txt': '📄', 'xml': '📦', 'yaml': '⚙️',
'yml': '⚙️', 'env': '🔑', 'sh': '💻', 'bat': '💻'
};
return icons[ext] || '📄';
}
/* -------------------------
SEARCH & REPLACE
------------------------- */
function toggleSearch() {
const searchBar = $('searchBar');
if (searchBar.classList.contains('hidden')) {
searchBar.classList.remove('hidden');
$('searchInput').focus();
} else {
searchBar.classList.add('hidden');
}
}
function performSearch() {
const searchTerm = $('searchInput').value;
const textarea = $('fileEditorContent');
if (!searchTerm || !textarea) return;
const text = textarea.value;
const regex = new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
const matches = [...text.matchAll(regex)];
$('searchInfo').textContent = matches.length > 0 ? `${matches.length} gefunden` : '0 gefunden';
if (matches.length > 0) {
const firstMatch = matches[0];
textarea.setSelectionRange(firstMatch.index, firstMatch.index + firstMatch[0].length);
textarea.focus();
}
}
function replaceOnce() {
const searchTerm = $('searchInput').value;
const replaceTerm = $('replaceInput').value;
const textarea = $('fileEditorContent');
if (!searchTerm || !textarea) return;
const text = textarea.value;
const newText = text.replace(new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'), replaceTerm);
textarea.value = newText;
pushToHistory(newText);
updateCurrentTab();
updateLineNumbers();
performSearch();
}
function replaceAll() {
const searchTerm = $('searchInput').value;
const replaceTerm = $('replaceInput').value;
const textarea = $('fileEditorContent');
if (!searchTerm || !textarea) return;
const text = textarea.value;
const newText = text.replace(new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), replaceTerm);
textarea.value = newText;
pushToHistory(newText);
updateCurrentTab();
updateLineNumbers();
performSearch();
}
async function saveCurrentFile(isAutoSave = false) {
if (!currentActiveTab) return;
const tab = openTabs[currentActiveTab];
// Prüfe ob es eine Bilddatei ist
if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(currentActiveTab)) {
alert('Bilder können nicht bearbeitet werden');
return;
}
const textarea = $('fileEditorContent');
const content = textarea.value;
if (!isAutoSave) setStatus('Speichert...');
try {
let response;
// Prüfe ob es eine Gitea-Datei ist
if (tab.isGitea) {
response = await window.electronAPI.writeGiteaFile({
owner: tab.owner,
repo: tab.repo,
path: currentActiveTab.replace(`gitea://${tab.owner}/${tab.repo}/`, ''),
content: content,
ref: 'main'
});
} else {
// Lokale Datei
response = await window.electronAPI.writeFile({
path: currentActiveTab,
content: content
});
}
if (response.ok) {
tab.originalContent = content;
tab.dirty = false;
// Push current state to history
pushToHistory(content);
renderTabs();
if (isAutoSave) {
showAutoSaveIndicator();
} else {
setStatus(`✓ Gespeichert: ${tab.name}`);
}
console.log('✅ File saved');
} else {
alert(`Fehler: ${response.error}`);
}
} catch (error) {
console.error('Error saving file:', error);
alert('Fehler beim Speichern');
}
}
function updateEditorStats() {
const textarea = $('fileEditorContent');
if (!textarea) return;
const lines = textarea.value.split('\n').length;
const startPos = textarea.selectionStart;
const textBeforeCursor = textarea.value.substring(0, startPos);
const line = textBeforeCursor.split('\n').length;
const col = startPos - textBeforeCursor.lastIndexOf('\n');
$('fileEditorCursor').textContent = `Zeile ${line}, Spalte ${col}`;
}
/* -------------------------
MARKDOWN PARSER
------------------------- */
function parseMarkdownToHTML(markdown) {
if (!markdown) return '';
let html = markdown;
// Check if content already contains HTML (starts with < or has closing tags)
const hasHTML = /<[a-zA-Z][\s\S]*>/.test(html);
if (!hasHTML) {
// Only escape and parse if no HTML present
html = html.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// Convert markdown patterns
// Headings: ### Title → <h3>Title</h3>
html = html.replace(/^###### (.*?)$/gm, '<h6>$1</h6>');
html = html.replace(/^##### (.*?)$/gm, '<h5>$1</h5>');
html = html.replace(/^#### (.*?)$/gm, '<h4>$1</h4>');
html = html.replace(/^### (.*?)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.*?)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.*?)$/gm, '<h1>$1</h1>');
// Horizontal rule: --- or *** or ___
html = html.replace(/^\-{3,}$/gm, '<hr>');
html = html.replace(/^\*{3,}$/gm, '<hr>');
html = html.replace(/^_{3,}$/gm, '<hr>');
// Bold: **text** or __text__
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/__(.*?)__/g, '<strong>$1</strong>');
// Italic: *text* or _text_ (but not within words)
html = html.replace(/\s\*(.*?)\*\s/g, ' <em>$1</em> ');
html = html.replace(/\s_(.*?)_\s/g, ' <em>$1</em> ');
// Convert line breaks to <br>
html = html.replace(/\n/g, '<br>');
// Wrap plain text in paragraphs (text not already in tags)
let lines = html.split('<br>');
lines = lines.map(line => {
line = line.trim();
if (line && !line.match(/^</) && line.length > 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 '<p>' + line + '</p>';
}
}
return line;
});
html = lines.join('<br>');
return html;
}
/* -------------------------
NAVIGATION & UI UPDATES
------------------------- */
function updateNavigationUI() {
const btnBack = $('btnBack');
if (!btnBack) return;
// Back Button zeigen, wenn wir in einem Repo oder tief in Ordnern sind
if (currentState.view === 'gitea-repo' ||
(currentState.view === 'gitea-list' && currentState.path !== '')) {
btnBack.classList.remove('hidden');
} else {
btnBack.classList.add('hidden');
}
}
/* -------------------------
GITEA CORE LOGIK (GRID)
------------------------- */
async function loadGiteaRepos() {
currentState.view = 'gitea-list';
currentState.path = '';
updateNavigationUI();
// Verstecke Commits & Releases-Buttons in Repo-Liste
const btnCommits = $('btnCommits');
const btnReleases = $('btnReleases');
if (btnCommits) btnCommits.classList.add('hidden');
if (btnReleases) btnReleases.classList.add('hidden');
// WICHTIG: Grid-Layout zurücksetzen
const grid = $('explorerGrid');
if (grid) {
grid.style.gridTemplateColumns = '';
}
setStatus('Loading Gitea repos...');
try {
const res = await window.electronAPI.listGiteaRepos();
if (!res.ok) {
setStatus('Failed to load repos: ' + (res.error || 'Unknown error'));
return;
}
const grid = $('explorerGrid');
if (!grid) return;
grid.innerHTML = '';
if (!res.repos || res.repos.length === 0) {
grid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: var(--text-muted);">Keine Repositories gefunden</div>';
setStatus('No repositories found');
return;
}
// --- NEU: Suchfeld für Projekte ---
const searchContainer = document.createElement('div');
searchContainer.style.cssText = 'grid-column: 1/-1; margin-bottom: 20px;';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = '🔍 Projekt suchen...';
searchInput.style.cssText = `
width: 100%;
padding: 12px 16px;
border-radius: var(--radius-md);
border: 1px solid rgba(255, 255, 255, 0.1);
background: var(--bg-tertiary);
color: var(--text-primary);
font-size: 14px;
outline: none;
box-sizing: border-box;
`;
// Search Focus Effekt
searchInput.addEventListener('focus', () => {
searchInput.style.borderColor = 'var(--accent-primary)';
});
searchInput.addEventListener('blur', () => {
searchInput.style.borderColor = 'rgba(255, 255, 255, 0.1)';
});
searchContainer.appendChild(searchInput);
grid.appendChild(searchContainer);
// Search Logic
searchInput.addEventListener('input', (e) => {
const val = e.target.value.toLowerCase();
const cards = grid.querySelectorAll('.item-card');
cards.forEach(card => {
const name = card.querySelector('.item-name').textContent.toLowerCase();
if (name.includes(val)) {
card.style.display = 'flex';
} else {
card.style.display = 'none';
}
});
});
// -----------------------------------
res.repos.forEach(repo => {
let owner = (repo.owner && (repo.owner.login || repo.owner.username)) || null;
let repoName = repo.name;
let cloneUrl = repo.clone_url || repo.clone_url_ssh;
const card = document.createElement('div');
card.className = 'item-card';
card.innerHTML = `<div class="item-icon">📦</div><div class="item-name">${repoName}</div>`;
card.dataset.cloneUrl = cloneUrl;
// --- Nativer Drag Start (Download) ---
card.draggable = true;
card.addEventListener('dragstart', async (ev) => {
ev.preventDefault();
setStatus(`Preparing download for ${repoName}...`);
showProgress(0, `Preparing ${repoName}...`);
try {
const resDrag = await window.electronAPI.prepareDownloadDrag({
owner,
repo: repoName,
path: ''
});
if (resDrag.ok) {
window.electronAPI.startNativeDrag(resDrag.tempPath);
setStatus('Ready to drag');
} else {
setStatus('Download preparation failed');
}
} catch (error) {
console.error('Drag preparation error:', error);
setStatus('Error preparing download');
} finally {
hideProgress();
}
});
// --- Nativer Drop (Upload in Repo Root) ---
card.addEventListener('dragover', (ev) => {
ev.preventDefault();
ev.stopPropagation();
card.classList.add('drag-target');
});
card.addEventListener('dragleave', (ev) => {
ev.preventDefault();
ev.stopPropagation();
card.classList.remove('drag-target');
});
card.addEventListener('drop', async (ev) => {
ev.preventDefault();
ev.stopPropagation();
card.classList.remove('drag-target');
const files = ev.dataTransfer.files;
if (!files || files.length === 0) {
setStatus("Keine Dateien zum Upload gefunden.");
return;
}
const paths = Array.from(files).map(f => f.path);
setStatus(`Starte Upload von ${paths.length} Elementen...`);
for (const p of paths) {
const baseName = p.split(/[\\/]/).pop();
showProgress(0, `Sende: ${baseName}`);
try {
const res = await window.electronAPI.uploadAndPush({
localFolder: p,
owner,
repo: repoName,
destPath: '',
cloneUrl,
branch: 'main'
});
if (!res.ok) {
console.error("Upload Fehler:", res.error);
setStatus("Fehler: " + res.error);
}
} catch (err) {
console.error("Kritischer Upload Fehler:", err);
setStatus("Upload fehlgeschlagen");
}
}
hideProgress();
setStatus('Upload abgeschlossen');
});
card.onclick = () => loadRepoContents(owner, repoName, '');
card.oncontextmenu = (ev) => showRepoContextMenu(ev, owner, repoName, cloneUrl, card);
grid.appendChild(card);
});
setStatus(`Loaded ${res.repos.length} repos`);
} catch (error) {
console.error('Error loading repos:', error);
setStatus('Error loading repositories');
}
}
async function loadRepoContents(owner, repo, path) {
currentState.view = 'gitea-repo';
currentState.owner = owner;
currentState.repo = repo;
currentState.path = path;
updateNavigationUI();
// Zeige Commits & Releases-Buttons wenn wir in einem Repo sind
const btnCommits = $('btnCommits');
const btnReleases = $('btnReleases');
if (btnCommits) {
btnCommits.classList.remove('hidden');
btnCommits.onclick = () => loadCommitHistory(owner, repo, 'main');
}
if (btnReleases) {
btnReleases.classList.remove('hidden');
btnReleases.onclick = () => loadRepoReleases(owner, repo);
}
// WICHTIG: Grid-Layout zurücksetzen
const grid = $('explorerGrid');
if (grid) {
grid.style.gridTemplateColumns = '';
}
setStatus(`Loading: /${path || 'root'}`);
try {
const res = await window.electronAPI.getGiteaRepoContents({
owner,
repo,
path,
ref: 'main'
});
if (!res.ok) {
setStatus('Error: ' + (res.error || 'Unknown error'));
return;
}
const grid = $('explorerGrid');
if (!grid) return;
grid.innerHTML = '';
if (!res.items || res.items.length === 0) {
grid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: var(--text-muted);">Leerer Ordner</div>';
setStatus('Empty folder');
return;
}
res.items.forEach(item => {
const icon = item.type === 'dir' ? '📁' : '📄';
const card = document.createElement('div');
card.className = 'item-card';
card.innerHTML = `<div class="item-icon">${icon}</div><div class="item-name">${item.name}</div>`;
// Drag für Files und Folders
if (item.type === 'dir') {
card.draggable = true;
card.addEventListener('dragstart', async (ev) => {
ev.preventDefault();
showProgress(0, `Preparing ${item.name}...`);
try {
const resDrag = await window.electronAPI.prepareDownloadDrag({
owner,
repo,
path: item.path
});
if (resDrag.ok) {
window.electronAPI.startNativeDrag(resDrag.tempPath);
}
} catch (error) {
console.error('Drag error:', error);
} finally {
hideProgress();
}
});
}
// Drop in Ordner
card.addEventListener('dragover', (ev) => {
ev.preventDefault();
ev.stopPropagation();
if (item.type === 'dir') card.classList.add('drag-target');
});
card.addEventListener('dragleave', (ev) => {
ev.preventDefault();
ev.stopPropagation();
card.classList.remove('drag-target');
});
card.addEventListener('drop', async (ev) => {
ev.preventDefault();
ev.stopPropagation();
card.classList.remove('drag-target');
if (item.type !== 'dir') return;
const files = ev.dataTransfer.files;
if (!files || files.length === 0) return;
const paths = Array.from(files).map(f => f.path);
const targetPath = item.path;
for (const p of paths) {
const baseName = p.split(/[\\/]/).pop();
showProgress(0, `Uploading ${baseName} to ${targetPath}...`);
try {
await window.electronAPI.uploadAndPush({
localFolder: p,
owner,
repo,
destPath: targetPath,
branch: 'main'
});
} catch (error) {
console.error('Upload error:', error);
}
}
hideProgress();
loadRepoContents(owner, repo, path);
});
if (item.type === 'dir') {
card.onclick = () => loadRepoContents(owner, repo, item.path);
} else {
card.onclick = () => openGiteaFileInEditor(owner, repo, item.path, item.name);
}
card.oncontextmenu = (ev) => showGiteaItemContextMenu(ev, item, owner, repo);
grid.appendChild(card);
});
setStatus(`Loaded ${res.items.length} items`);
} catch (error) {
console.error('Error loading repo contents:', error);
setStatus('Error loading contents');
}
}
/* -------------------------
LOKALE LOGIK
------------------------- */
async function selectLocalFolder() {
try {
const folder = await window.electronAPI.selectFolder();
if (!folder) return;
selectedFolder = folder;
setStatus('Local: ' + folder);
currentState.view = 'local';
updateNavigationUI();
await refreshLocalTree(folder);
await loadBranches(folder);
} catch (error) {
console.error('Error selecting folder:', error);
setStatus('Error selecting folder');
}
}
async function refreshLocalTree(folder) {
try {
const res = await window.electronAPI.getFileTree({
folder,
exclude: ['node_modules', '.git'],
maxDepth: 5
});
const grid = $('explorerGrid');
if (!grid) return;
grid.innerHTML = '';
if (!res.ok) {
setStatus('Error loading local files');
return;
}
if (!res.tree || res.tree.length === 0) {
grid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: var(--text-muted);">Keine Dateien gefunden</div>';
return;
}
res.tree.forEach(node => {
const card = document.createElement('div');
card.className = 'item-card';
card.innerHTML = `
<div class="item-icon">${node.isDirectory ? '📁' : '📄'}</div>
<div class="item-name">${node.name}</div>
`;
card.onclick = async () => {
if (!node.isDirectory) {
openFileEditor(node.path, node.name);
}
};
grid.appendChild(card);
});
} catch (error) {
console.error('Error refreshing tree:', error);
setStatus('Error loading file tree');
}
}
/* -------------------------
GIT ACTIONS
------------------------- */
async function pushLocalFolder() {
if (!selectedFolder) {
alert('Select local folder first');
return;
}
const branch = $('branchSelect')?.value || 'main';
const repoName = $('repoName')?.value;
const platform = $('platform')?.value;
setStatus('Pushing...');
showProgress(0, 'Starting push...');
try {
const res = await window.electronAPI.pushProject({
folder: selectedFolder,
branch,
repoName,
platform
});
if (res.ok) {
setStatus('Push succeeded');
} else {
setStatus('Push failed: ' + (res.error || 'Unknown error'));
}
} catch (error) {
console.error('Push error:', error);
setStatus('Push failed');
} finally {
hideProgress();
}
}
async function loadBranches(folder) {
try {
const res = await window.electronAPI.getBranches({ folder });
const sel = $('branchSelect');
if (!sel) return;
sel.innerHTML = '';
if (res.ok && res.branches) {
res.branches.forEach(b => {
const option = document.createElement('option');
option.value = b;
option.textContent = b;
sel.appendChild(option);
});
}
} catch (error) {
console.error('Error loading branches:', error);
}
}
async function loadCommitLogs(folder) {
try {
const res = await window.electronAPI.getCommitLogs({ folder });
const container = $('logs');
if (!container) return;
container.innerHTML = '';
if (res.ok && res.logs) {
res.logs.forEach(l => {
const d = document.createElement('div');
d.className = 'log-item';
d.innerText = l;
container.appendChild(d);
});
}
} catch (error) {
console.error('Error loading logs:', error);
}
}
/* -------------------------
CONTEXT MENÜS
------------------------- */
function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) {
ev.preventDefault();
ev.stopPropagation();
const old = $('ctxMenu');
if (old) old.remove();
const menu = document.createElement('div');
menu.id = 'ctxMenu';
menu.className = 'context-menu';
menu.style.left = ev.clientX + 'px';
menu.style.top = ev.clientY + 'px';
const createMenuItem = (icon, text, onClick, color = null) => {
const item = document.createElement('div');
item.className = 'context-item';
item.innerHTML = `${icon} ${text}`;
if (color) item.style.color = color;
item.onclick = onClick;
return item;
};
const uploadItem = createMenuItem('🚀', 'Folder hier hochladen', async () => {
menu.remove();
try {
const sel = await window.electronAPI.selectFolder();
if (sel) {
showProgress(0, 'Upload...');
await window.electronAPI.uploadAndPush({
localFolder: sel,
owner,
repo: repoName,
destPath: '',
cloneUrl,
branch: 'main'
});
hideProgress();
setStatus('Upload complete');
}
} catch (error) {
console.error('Upload error:', error);
hideProgress();
setStatus('Upload failed');
}
});
const deleteItem = createMenuItem('🗑️', 'Repo löschen', async () => {
menu.remove();
if (confirm(`Delete ${repoName}?`)) {
try {
const res = await window.electronAPI.deleteGiteaRepo({ owner, repo: repoName });
if (res.ok) {
element.remove();
setStatus('Repository deleted');
} else {
setStatus('Delete failed: ' + res.error);
}
} catch (error) {
console.error('Delete error:', error);
setStatus('Delete failed');
}
}
}, '#ef4444');
menu.appendChild(uploadItem);
menu.appendChild(deleteItem);
document.body.appendChild(menu);
setTimeout(() => {
document.addEventListener('click', () => menu.remove(), { once: true });
}, 10);
}
function showGiteaItemContextMenu(ev, item, owner, repo) {
ev.preventDefault();
ev.stopPropagation();
const old = $('ctxMenu');
if (old) old.remove();
const menu = document.createElement('div');
menu.id = 'ctxMenu';
menu.className = 'context-menu';
menu.style.left = ev.clientX + 'px';
menu.style.top = ev.clientY + 'px';
if (item.type === 'file') {
const downloadItem = document.createElement('div');
downloadItem.className = 'context-item';
downloadItem.innerHTML = '📥 Download File';
downloadItem.onclick = async () => {
menu.remove();
try {
const res = await window.electronAPI.downloadGiteaFile({
owner,
repo,
path: item.path
});
if (res.ok) {
setStatus(`Saved to ${res.savedTo}`);
} else {
setStatus('Download failed');
}
} catch (error) {
console.error('Download error:', error);
setStatus('Download failed');
}
};
menu.appendChild(downloadItem);
}
document.body.appendChild(menu);
setTimeout(() => {
document.addEventListener('click', () => menu.remove(), { once: true });
}, 10);
}
/* -------------------------
HELPER FUNCTIONS
------------------------- */
function ppathBasename(p) {
try {
return p.split(/[\\/]/).pop();
} catch (_) {
return p;
}
}
async function previewGiteaFile(owner, repo, filePath) {
try {
const res = await window.electronAPI.getGiteaFileContent({
owner,
repo,
path: filePath,
ref: 'main'
});
if (res.ok) {
console.log("Content of", filePath, ":", res.content);
setStatus(`Previewed: ${filePath}`);
} else {
setStatus('Preview failed');
}
} catch (error) {
console.error('Preview error:', error);
setStatus('Preview failed');
}
}
async function createRepoHandler() {
const name = $('repoName')?.value?.trim();
if (!name) {
alert('Name required');
return;
}
setStatus('Creating repository...');
try {
const res = await window.electronAPI.createRepo({
name,
platform: $('platform').value,
license: $('licenseSelect')?.value || '',
autoInit: $('createReadme')?.checked || true
});
if (res.ok) {
$('repoActionModal')?.classList.add('hidden');
setStatus('Repository created');
loadGiteaRepos();
} else {
setStatus('Create failed: ' + (res.error || 'Unknown error'));
}
} catch (error) {
console.error('Create repo error:', error);
setStatus('Create failed');
}
}
/* -------------------------
GLOBALER DROP-HANDLER FÜR REPO-ANSICHT
------------------------- */
function setupGlobalDropZone() {
const main = $('main');
if (!main) return;
// Visual feedback beim Drag über das Fenster
let dragCounter = 0;
main.addEventListener('dragenter', (ev) => {
// Nur in Repo-Ansicht aktiv
if (currentState.view !== 'gitea-repo') return;
dragCounter++;
if (dragCounter === 1) {
main.classList.add('drop-active');
}
});
main.addEventListener('dragleave', (ev) => {
if (currentState.view !== 'gitea-repo') return;
dragCounter--;
if (dragCounter === 0) {
main.classList.remove('drop-active');
}
});
main.addEventListener('dragover', (ev) => {
if (currentState.view !== 'gitea-repo') return;
ev.preventDefault();
});
main.addEventListener('drop', async (ev) => {
if (currentState.view !== 'gitea-repo') return;
ev.preventDefault();
ev.stopPropagation();
dragCounter = 0;
main.classList.remove('drop-active');
const files = ev.dataTransfer.files;
if (!files || files.length === 0) {
setStatus("Keine Dateien zum Upload gefunden.");
return;
}
// Upload in aktuellen Pfad
const owner = currentState.owner;
const repo = currentState.repo;
const targetPath = currentState.path || '';
const paths = Array.from(files).map(f => f.path);
setStatus(`Uploading ${paths.length} items to /${targetPath || 'root'}...`);
for (const p of paths) {
const baseName = p.split(/[\\/]/).pop();
showProgress(0, `Uploading: ${baseName}`);
try {
const res = await window.electronAPI.uploadAndPush({
localFolder: p,
owner,
repo,
destPath: targetPath,
branch: 'main'
});
if (!res.ok) {
console.error("Upload error:", res.error);
setStatus("Error: " + res.error);
} else {
setStatus(`Uploaded: ${baseName}`);
}
} catch (err) {
console.error("Critical upload error:", err);
setStatus("Upload failed");
}
}
hideProgress();
// Refresh current view
setTimeout(() => {
loadRepoContents(owner, repo, targetPath);
}, 1000);
});
}
/* -------------------------
INITIALISIERUNG
------------------------- */
window.addEventListener('DOMContentLoaded', async () => {
// Prevent default drag/drop on document (except in repo view)
document.addEventListener('dragover', e => {
if (currentState.view !== 'gitea-repo') {
e.preventDefault();
}
});
document.addEventListener('drop', e => {
if (currentState.view !== 'gitea-repo') {
e.preventDefault();
}
});
// Load credentials
try {
const creds = await window.electronAPI.loadCredentials();
if (creds) {
if ($('githubToken')) $('githubToken').value = creds.githubToken || '';
if ($('giteaToken')) $('giteaToken').value = creds.giteaToken || '';
if ($('giteaURL')) $('giteaURL').value = creds.giteaURL || '';
}
} catch (error) {
console.error('Error loading credentials:', error);
}
// Event Handlers
if ($('btnLoadGiteaRepos')) {
$('btnLoadGiteaRepos').onclick = loadGiteaRepos;
}
if ($('btnSelectFolder')) {
$('btnSelectFolder').onclick = selectLocalFolder;
}
if ($('btnPush')) {
$('btnPush').onclick = pushLocalFolder;
}
if ($('btnCreateRepo')) {
$('btnCreateRepo').onclick = createRepoHandler;
}
if ($('btnBack')) {
$('btnBack').onclick = () => {
if (currentState.view === 'gitea-repo') {
if (currentState.path === '' || currentState.path === '/') {
loadGiteaRepos();
} else {
const parts = currentState.path.split('/').filter(p => p);
parts.pop();
loadRepoContents(currentState.owner, currentState.repo, parts.join('/'));
}
}
};
}
// Modal controls
if ($('btnSettings')) {
$('btnSettings').onclick = () => {
$('settingsModal').classList.remove('hidden');
};
}
if ($('btnCloseSettings')) {
$('btnCloseSettings').onclick = () => {
$('settingsModal').classList.add('hidden');
};
}
if ($('btnOpenRepoActions')) {
$('btnOpenRepoActions').onclick = () => {
$('repoActionModal').classList.remove('hidden');
};
}
if ($('btnCloseRepoActions')) {
$('btnCloseRepoActions').onclick = () => {
$('repoActionModal').classList.add('hidden');
};
}
if ($('btnSaveSettings')) {
$('btnSaveSettings').onclick = async () => {
try {
const data = {
githubToken: $('githubToken').value,
giteaToken: $('giteaToken').value,
giteaURL: $('giteaURL').value
};
await window.electronAPI.saveCredentials(data);
$('settingsModal').classList.add('hidden');
setStatus('Settings saved');
} catch (error) {
console.error('Error saving settings:', error);
setStatus('Save failed');
}
};
}
// FILE EDITOR EVENT LISTENERS
if ($('btnCloseEditor')) {
$('btnCloseEditor').onclick = closeFileEditor;
}
if ($('btnEditorSave')) {
$('btnEditorSave').onclick = () => saveCurrentFile(false);
}
if ($('btnEditorSearch')) {
$('btnEditorSearch').onclick = toggleSearch;
}
if ($('btnReplace')) {
$('btnReplace').onclick = replaceOnce;
}
if ($('btnReplaceAll')) {
$('btnReplaceAll').onclick = replaceAll;
}
if ($('btnCloseSearch')) {
$('btnCloseSearch').onclick = () => {
$('searchBar').classList.add('hidden');
};
}
if ($('searchInput')) {
$('searchInput').addEventListener('input', performSearch);
$('searchInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
performSearch();
}
});
}
if ($('btnDiscardEdit')) {
$('btnDiscardEdit').onclick = () => {
const tab = openTabs[currentActiveTab];
if (tab) {
tab.content = tab.originalContent;
tab.dirty = false;
tab.history = [tab.originalContent];
tab.historyIndex = 0;
updateEditor();
}
};
}
// Modal wird mit pointer-events: none nicht geschlossen durch Klicks
// Der Grid bleibt voll interaktiv für neue Tabs
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
// Ctrl+S - Save
if (e.key === 's') {
e.preventDefault();
if (currentActiveTab) {
saveCurrentFile(false);
}
}
// Ctrl+F - Search
if (e.key === 'f') {
e.preventDefault();
if (currentActiveTab) {
toggleSearch();
}
}
// Ctrl+H - Replace
if (e.key === 'h') {
e.preventDefault();
if (currentActiveTab) {
toggleSearch();
$('replaceInput').focus();
}
}
}
// ESC - Close search
if (e.key === 'Escape') {
if (!$('searchBar').classList.contains('hidden')) {
$('searchBar').classList.add('hidden');
}
}
});
// Progress listeners
window.electronAPI.onFolderUploadProgress(p => {
showProgress(p.percent, `Upload: ${p.processed}/${p.total}`);
});
window.electronAPI.onFolderDownloadProgress(p => {
showProgress(p.percent, `Download: ${p.processed}/${p.total}`);
});
// Setup globalen Drop-Handler für Repo-Ansicht
setupGlobalDropZone();
setStatus('Ready');
initUpdater(); // Updater initialisieren
updateNavigationUI();
});
/* ================================
RELEASE MANAGEMENT UI FUNCTIONS
Füge dies zu renderer.js hinzu
================================ */
let currentReleaseView = {
owner: null,
repo: null
};
/* -------------------------
RELEASES LADEN & ANZEIGEN
------------------------- */
async function loadRepoReleases(owner, repo) {
currentReleaseView.owner = owner;
currentReleaseView.repo = repo;
setStatus('Loading releases...');
try {
const res = await window.electronAPI.listReleases({ owner, repo });
if (!res.ok) {
setStatus('Error loading releases: ' + res.error);
return;
}
const grid = $('explorerGrid');
if (!grid) return;
// Header mit "New Release" Button
grid.innerHTML = `
<div style="grid-column: 1/-1; display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0; color: var(--text-primary);">📦 Releases für ${repo}</h2>
<button class="btn-new-release" style="
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;
">
🚀 New Release
</button>
</div>
`;
const newBtn = grid.querySelector('.btn-new-release');
newBtn.onclick = () => showCreateReleaseModal(owner, repo);
if (!res.releases || res.releases.length === 0) {
grid.innerHTML += '<div style="grid-column: 1/-1; text-align: center; padding: 60px; color: var(--text-muted); font-size: 16px;">📭 Noch keine Releases veröffentlicht</div>';
setStatus('No releases');
return;
}
// Releases als Cards darstellen
res.releases.forEach((release, index) => {
const card = createReleaseCard(release, index === 0);
grid.appendChild(card);
});
setStatus(`${res.releases.length} release(s) loaded`);
} catch (error) {
console.error('Error loading releases:', error);
setStatus('Failed to load releases');
}
}
function createReleaseCard(release, isLatest) {
const card = document.createElement('div');
card.className = 'release-card';
card.style.cssText = `
grid-column: 1/-1;
background: var(--bg-tertiary);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-lg);
transition: all var(--transition-normal);
`;
// Header mit Tag und Badges
const header = document.createElement('div');
header.style.cssText = 'display: flex; gap: 10px; align-items: center; margin-bottom: 12px;';
const tag = document.createElement('span');
tag.textContent = release.tag_name;
tag.style.cssText = `
background: var(--accent-gradient);
color: #000;
padding: 6px 16px;
border-radius: 20px;
font-weight: 700;
font-size: 14px;
`;
header.appendChild(tag);
if (isLatest) {
const latestBadge = document.createElement('span');
latestBadge.textContent = 'LATEST';
latestBadge.style.cssText = `
background: var(--success);
color: #000;
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 700;
`;
header.appendChild(latestBadge);
}
if (release.prerelease) {
const preBadge = document.createElement('span');
preBadge.textContent = 'PRE-RELEASE';
preBadge.style.cssText = `
background: var(--warning);
color: #000;
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 700;
`;
header.appendChild(preBadge);
}
if (release.draft) {
const draftBadge = document.createElement('span');
draftBadge.textContent = 'DRAFT';
draftBadge.style.cssText = `
background: rgba(255, 255, 255, 0.2);
color: var(--text-primary);
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 700;
`;
header.appendChild(draftBadge);
}
card.appendChild(header);
// Title
const title = document.createElement('h3');
title.textContent = release.name || release.tag_name;
title.style.cssText = 'margin: 0 0 12px 0; color: var(--text-primary); font-size: 20px;';
card.appendChild(title);
// Body (Release Notes)
if (release.body) {
const body = document.createElement('div');
body.className = 'release-body';
body.innerHTML = parseMarkdownToHTML(release.body);
card.appendChild(body);
}
// Assets
if (release.assets && release.assets.length > 0) {
const assetsContainer = document.createElement('div');
assetsContainer.style.cssText = `
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
`;
const assetsTitle = document.createElement('div');
assetsTitle.textContent = '📦 Assets';
assetsTitle.style.cssText = 'font-weight: 600; margin-bottom: 12px; color: var(--text-primary);';
assetsContainer.appendChild(assetsTitle);
release.assets.forEach(asset => {
const assetItem = document.createElement('div');
assetItem.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background: rgba(255, 255, 255, 0.03);
border-radius: var(--radius-sm);
margin-bottom: 8px;
`;
const assetName = document.createElement('span');
assetName.textContent = `📎 ${asset.name}`;
assetName.style.cssText = 'color: var(--text-primary);';
const assetSize = document.createElement('span');
assetSize.textContent = formatBytes(asset.size || 0);
assetSize.style.cssText = 'color: var(--text-muted); font-size: 12px; margin-left: 12px;';
const downloadBtn = document.createElement('button');
downloadBtn.textContent = '⬇️ Download';
downloadBtn.style.cssText = `
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 6px 12px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 12px;
`;
downloadBtn.onclick = () => {
if (asset.browser_download_url) {
window.open(asset.browser_download_url, '_blank');
}
};
const deleteAssetBtn = document.createElement('button');
deleteAssetBtn.textContent = '🗑️';
deleteAssetBtn.style.cssText = `
background: transparent;
color: var(--danger);
border: 1px solid var(--danger);
padding: 6px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 12px;
margin-left: 8px;
`;
deleteAssetBtn.onclick = async () => {
if (confirm(`Delete asset "${asset.name}"?`)) {
const res = await window.electronAPI.deleteReleaseAsset({
owner: currentReleaseView.owner,
repo: currentReleaseView.repo,
assetId: asset.id
});
if (res.ok) {
assetItem.remove();
setStatus('Asset deleted');
}
}
};
const leftSide = document.createElement('div');
leftSide.style.cssText = 'display: flex; align-items: center; gap: 12px;';
leftSide.appendChild(assetName);
leftSide.appendChild(assetSize);
const rightSide = document.createElement('div');
rightSide.style.cssText = 'display: flex; gap: 8px;';
rightSide.appendChild(downloadBtn);
rightSide.appendChild(deleteAssetBtn);
assetItem.appendChild(leftSide);
assetItem.appendChild(rightSide);
assetsContainer.appendChild(assetItem);
});
card.appendChild(assetsContainer);
}
// Meta Info
const meta = document.createElement('div');
meta.style.cssText = `
display: flex;
gap: 20px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
color: var(--text-muted);
font-size: 12px;
`;
const date = new Date(release.created_at);
const dateStr = date.toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
meta.innerHTML = `
<span>📅 ${dateStr}</span>
<span>👤 ${release.author?.login || 'Unknown'}</span>
`;
card.appendChild(meta);
// Action Buttons
const actions = document.createElement('div');
actions.style.cssText = 'display: flex; gap: 12px; margin-top: 16px;';
const downloadArchiveBtn = document.createElement('button');
downloadArchiveBtn.textContent = '📦 Download ZIP';
downloadArchiveBtn.style.cssText = `
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 8px 16px;
border-radius: var(--radius-md);
cursor: pointer;
font-weight: 600;
`;
downloadArchiveBtn.onclick = async () => {
const res = await window.electronAPI.downloadReleaseArchive({
owner: currentReleaseView.owner,
repo: currentReleaseView.repo,
tag: release.tag_name
});
if (res.ok) {
setStatus(`Downloaded to ${res.savedTo}`);
}
};
const addAssetBtn = document.createElement('button');
addAssetBtn.textContent = '📎 Add Asset';
addAssetBtn.style.cssText = `
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 8px 16px;
border-radius: var(--radius-md);
cursor: pointer;
font-weight: 600;
`;
addAssetBtn.onclick = () => showUploadAssetDialog(release);
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '🗑️ Delete';
deleteBtn.style.cssText = `
background: transparent;
color: var(--danger);
border: 1px solid var(--danger);
padding: 8px 16px;
border-radius: var(--radius-md);
cursor: pointer;
font-weight: 600;
margin-left: auto;
`;
deleteBtn.onclick = async () => {
if (confirm(`Delete release "${release.name || release.tag_name}"?`)) {
const res = await window.electronAPI.deleteRelease({
owner: currentReleaseView.owner,
repo: currentReleaseView.repo,
releaseId: release.id
});
if (res.ok) {
card.remove();
setStatus('Release deleted');
}
}
};
actions.appendChild(downloadArchiveBtn);
actions.appendChild(addAssetBtn);
actions.appendChild(deleteBtn);
card.appendChild(actions);
return card;
}
/* -------------------------
CREATE RELEASE MODAL
------------------------- */
function showCreateReleaseModal(owner, repo) {
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="card" style="width: 600px; max-width: 90vw;">
<h2>🚀 Neues Release erstellen</h2>
<div class="input-group">
<label>Tag Version *</label>
<input id="releaseTag" type="text" placeholder="v1.0.0">
</div>
<div class="input-group">
<label>Release Name</label>
<input id="releaseName" type="text" placeholder="Version 1.0.0">
</div>
<div class="input-group">
<label>Release Notes</label>
<textarea id="releaseBody" rows="8" placeholder="## Was ist neu?
- Feature 1
- Feature 2
- Bug Fixes" style="
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;
"></textarea>
</div>
<div class="input-group">
<label>Target Branch</label>
<input id="releaseTarget" type="text" value="main" placeholder="main">
</div>
<div class="input-group" style="display: flex; gap: 20px;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="releasePrerelease"> Pre-Release
</label>
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="releaseDraft"> Draft (nicht veröffentlichen)
</label>
</div>
<div class="modal-buttons">
<button id="btnCreateRelease">Erstellen & Veröffentlichen</button>
<button id="btnCancelRelease">Abbrechen</button>
</div>
</div>
`;
document.body.appendChild(modal);
$('btnCreateRelease').onclick = async () => {
const tag = $('releaseTag').value.trim();
const name = $('releaseName').value.trim() || tag;
const body = $('releaseBody').value.trim();
const target = $('releaseTarget').value.trim() || 'main';
const prerelease = $('releasePrerelease').checked;
const draft = $('releaseDraft').checked;
if (!tag) {
alert('Tag Version ist erforderlich!');
return;
}
setStatus('Creating release...');
try {
const res = await window.electronAPI.createRelease({
owner,
repo,
tag_name: tag,
name,
body,
target_commitish: target,
prerelease,
draft
});
if (res.ok) {
modal.remove();
setStatus('Release created!');
loadRepoReleases(owner, repo); // Reload
} else {
setStatus('Failed: ' + res.error);
}
} catch (error) {
console.error('Create release error:', error);
setStatus('Create failed');
}
};
$('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 {
setStatus('Upload failed: ' + uploadRes.error);
}
} catch (error) {
console.error('Upload asset error:', error);
hideProgress();
setStatus('Upload failed');
}
}
/* -------------------------
HELPER FUNCTIONS
------------------------- */
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
/* -------------------------
INTEGRATION IN REPO VIEW
Füge "Releases" Tab zum Repo hinzu
------------------------- */
// Modifiziere die loadRepoContents Funktion um einen Releases-Button hinzuzufügen:
// Nach dem Laden eines Repos, zeige einen Button "View Releases" an
/* ================================
COMMIT HISTORY VISUALIZATION UI
Füge dies zu renderer.js hinzu
================================ */
let currentCommitView = {
owner: null,
repo: null,
branch: 'main',
commits: [],
selectedCommit: null
};
/* -------------------------
COMMIT HISTORY LADEN
------------------------- */
async function loadCommitHistory(owner, repo, branch = 'main') {
currentCommitView.owner = owner;
currentCommitView.repo = repo;
currentCommitView.branch = branch;
setStatus('Loading commit history...');
try {
const res = await window.electronAPI.getCommits({
owner,
repo,
branch,
limit: 100
});
if (!res.ok) {
setStatus('Error loading commits: ' + res.error);
return;
}
currentCommitView.commits = res.commits;
renderCommitHistoryView();
setStatus(`${res.commits.length} commits loaded`);
} catch (error) {
console.error('Error loading commit history:', error);
setStatus('Failed to load commits');
}
}
function renderCommitHistoryView() {
const grid = $('explorerGrid');
if (!grid) return;
grid.innerHTML = '';
grid.style.gridTemplateColumns = '1fr';
// Header mit Search und Branch-Selector
const header = document.createElement('div');
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
gap: 16px;
flex-wrap: wrap;
`;
header.innerHTML = `
<h2 style="margin: 0; color: var(--text-primary); display: flex; align-items: center; gap: 12px;">
📊 Commit History
<span style="font-size: 14px; color: var(--text-muted); font-weight: 400;">
${currentCommitView.repo} / ${currentCommitView.branch}
</span>
</h2>
<div style="display: flex; gap: 12px; flex: 1; max-width: 600px;">
<input
type="text"
id="commitSearch"
placeholder="🔍 Search commits (message, author)..."
style="
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;
"
/>
<button id="btnClearSearch" style="
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;
">Clear</button>
</div>
`;
grid.appendChild(header);
// Search-Handler
const searchInput = header.querySelector('#commitSearch');
const clearBtn = header.querySelector('#btnClearSearch');
let searchTimeout;
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
handleCommitSearch(e.target.value);
}, 300);
});
clearBtn.onclick = () => {
searchInput.value = '';
renderCommitTimeline(currentCommitView.commits);
};
// Timeline Container
const timelineContainer = document.createElement('div');
timelineContainer.id = 'commitTimeline';
timelineContainer.style.cssText = `
position: relative;
max-width: 100%;
`;
grid.appendChild(timelineContainer);
// Initial render
renderCommitTimeline(currentCommitView.commits);
}
function renderCommitTimeline(commits) {
const container = $('commitTimeline');
if (!container) return;
container.innerHTML = '';
if (!commits || commits.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 60px; color: var(--text-muted); font-size: 16px;">📭 No commits found</div>';
return;
}
// Timeline mit Cards
commits.forEach((commit, index) => {
const card = createCommitCard(commit, index);
container.appendChild(card);
});
}
function createCommitCard(commit, index) {
const card = document.createElement('div');
card.className = 'commit-card';
card.dataset.sha = commit.sha;
const isEven = index % 2 === 0;
card.style.cssText = `
position: relative;
padding-left: 60px;
margin-bottom: 32px;
cursor: pointer;
transition: all var(--transition-normal);
`;
// Timeline dot
const dot = document.createElement('div');
dot.style.cssText = `
position: absolute;
left: 18px;
top: 0;
width: 16px;
height: 16px;
background: var(--accent-primary);
border: 3px solid var(--bg-primary);
border-radius: 50%;
z-index: 2;
box-shadow: 0 0 0 4px var(--bg-tertiary);
`;
card.appendChild(dot);
// Timeline line
if (index < currentCommitView.commits.length - 1) {
const line = document.createElement('div');
line.style.cssText = `
position: absolute;
left: 25px;
top: 16px;
width: 2px;
height: calc(100% + 32px);
background: linear-gradient(180deg, var(--accent-primary) 0%, rgba(0, 212, 255, 0.2) 100%);
z-index: 1;
`;
card.appendChild(line);
}
// Content card
const content = document.createElement('div');
content.className = 'commit-content';
content.style.cssText = `
background: var(--bg-tertiary);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
transition: all var(--transition-normal);
`;
// Commit message
const message = commit.commit?.message || commit.message || 'No message';
const shortMessage = message.split('\n')[0]; // First line only
const messageEl = document.createElement('div');
messageEl.style.cssText = `
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 12px;
line-height: 1.4;
`;
messageEl.textContent = shortMessage;
content.appendChild(messageEl);
// Meta info
const meta = document.createElement('div');
meta.style.cssText = `
display: flex;
gap: 20px;
flex-wrap: wrap;
font-size: 13px;
color: var(--text-muted);
margin-bottom: 12px;
`;
const author = commit.commit?.author?.name || commit.author?.login || 'Unknown';
const date = new Date(commit.commit?.author?.date || commit.created_at);
const dateStr = formatRelativeTime(date);
const sha = commit.sha?.substring(0, 7) || '???????';
meta.innerHTML = `
<span style="display: flex; align-items: center; gap: 6px;">
👤 <strong>${author}</strong>
</span>
<span style="display: flex; align-items: center; gap: 6px;">
🕐 ${dateStr}
</span>
<span style="display: flex; align-items: center; gap: 6px; font-family: monospace; background: rgba(255,255,255,0.05); padding: 2px 8px; border-radius: 4px;">
#${sha}
</span>
`;
content.appendChild(meta);
// Stats (if available)
if (commit.stats) {
const stats = document.createElement('div');
stats.style.cssText = `
display: flex;
gap: 16px;
font-size: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
`;
stats.innerHTML = `
<span style="color: var(--success);">+${commit.stats.additions || 0}</span>
<span style="color: var(--danger);">-${commit.stats.deletions || 0}</span>
<span style="color: var(--text-muted);">${commit.stats.total || 0} changes</span>
`;
content.appendChild(stats);
}
card.appendChild(content);
// Hover effect
card.addEventListener('mouseenter', () => {
content.style.borderColor = 'var(--accent-primary)';
content.style.transform = 'translateX(4px)';
content.style.boxShadow = 'var(--shadow-md)';
});
card.addEventListener('mouseleave', () => {
content.style.borderColor = 'rgba(255, 255, 255, 0.1)';
content.style.transform = 'translateX(0)';
content.style.boxShadow = 'none';
});
// Click to show details
card.onclick = () => showCommitDetails(commit);
return card;
}
/* -------------------------
COMMIT DETAILS & DIFF VIEWER
------------------------- */
async function showCommitDetails(commit) {
currentCommitView.selectedCommit = commit;
const modal = document.createElement('div');
modal.className = 'modal commit-modal';
modal.innerHTML = `
<div class="card" style="width: 90vw; max-width: 1200px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0;">📋 Commit Details</h2>
<button id="btnCloseCommitModal" style="
background: transparent;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--text-muted);
">✕</button>
</div>
<div style="overflow-y: auto; flex: 1;">
<div id="commitDetailsContent" style="padding-bottom: 20px;">
<div style="text-align: center; padding: 40px; color: var(--text-muted);">
Loading commit details...
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
$('btnCloseCommitModal').onclick = () => modal.remove();
modal.onclick = (e) => {
if (e.target === modal) modal.remove();
};
// Load commit details
await loadCommitDetailsContent(commit);
}
async function loadCommitDetailsContent(commit) {
const container = $('commitDetailsContent');
if (!container) return;
try {
// Check if this is local git or Gitea repo
let diffRes, filesRes;
if (selectedFolder) {
// Local Git repository
const details = await window.electronAPI.getLocalCommitDetails({
folderPath: selectedFolder,
sha: commit.sha || commit.hash
});
diffRes = { ok: true, diff: details?.diff || '' };
filesRes = { ok: true, files: details?.fileChanges?.files || [], stats: { additions: details?.fileChanges?.insertions || 0, deletions: details?.fileChanges?.deletions || 0 } };
} else {
// Gitea repository
const [diff, files] = await Promise.all([
window.electronAPI.getCommitDiff({
owner: currentCommitView.owner,
repo: currentCommitView.repo,
sha: commit.sha
}),
window.electronAPI.getCommitFiles({
owner: currentCommitView.owner,
repo: currentCommitView.repo,
sha: commit.sha
})
]);
diffRes = diff;
filesRes = files;
}
container.innerHTML = '';
// Commit info header
const header = document.createElement('div');
header.style.cssText = `
background: var(--bg-tertiary);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
margin-bottom: 24px;
`;
const message = commit.commit?.message || commit.message || 'No message';
const author = commit.commit?.author?.name || commit.author?.login || 'Unknown';
const email = commit.commit?.author?.email || '';
const date = new Date(commit.commit?.author?.date || commit.created_at);
const sha = commit.sha || '';
header.innerHTML = `
<h3 style="margin: 0 0 16px 0; font-size: 20px; line-height: 1.4;">${escapeHtml(message)}</h3>
<div style="display: flex; gap: 24px; font-size: 14px; color: var(--text-muted); flex-wrap: wrap;">
<span>👤 <strong>${escapeHtml(author)}</strong> ${email ? `&lt;${escapeHtml(email)}&gt;` : ''}</span>
<span>🕐 ${date.toLocaleString()}</span>
<span style="font-family: monospace; background: rgba(255,255,255,0.05); padding: 4px 12px; border-radius: 6px;">${sha.substring(0, 7)}</span>
</div>
`;
container.appendChild(header);
// File changes summary
if (filesRes.ok && filesRes.files && Array.isArray(filesRes.files) && filesRes.files.length > 0) {
const filesHeader = document.createElement('div');
filesHeader.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
`;
filesHeader.innerHTML = `
<h4 style="margin: 0; display: flex; align-items: center; gap: 8px;">
📁 Changed Files (${filesRes.files.length})
</h4>
<div style="display: flex; gap: 16px; font-size: 13px;">
<span style="color: var(--success);">+${filesRes.stats?.additions || 0}</span>
<span style="color: var(--danger);">-${filesRes.stats?.deletions || 0}</span>
</div>
`;
container.appendChild(filesHeader);
// File list
const fileList = document.createElement('div');
fileList.style.cssText = 'margin-bottom: 24px;';
filesRes.files.forEach(file => {
const fileItem = document.createElement('div');
fileItem.style.cssText = `
display: flex;
justify-content: space-between;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.03);
border-radius: var(--radius-sm);
margin-bottom: 4px;
font-size: 13px;
`;
const changeType = file.changes === file.insertions ? 'added' :
file.changes === file.deletions ? 'deleted' : 'modified';
const icon = changeType === 'added' ? '🆕' :
changeType === 'deleted' ? '🗑️' : '📝';
fileItem.innerHTML = `
<span style="font-family: monospace; color: var(--text-primary);">${icon} ${escapeHtml(file.file)}</span>
<span style="color: var(--text-muted);">
<span style="color: var(--success);">+${file.insertions}</span>
<span style="color: var(--danger); margin-left: 8px;">-${file.deletions}</span>
</span>
`;
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 = `<div style="text-align: center; padding: 40px; color: var(--danger);">
<p>❌ Error loading commit details</p>
<p style="font-size: 12px; color: var(--text-muted); margin-top: 12px;">
Source: ${isLocalGit}<br>
Error: ${escapeHtml(errorMsg)}
</p>
</div>`;
}
}
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 += `<div style="color: ${color}; background: ${bgColor}; padding: 2px 4px;">${escapeHtml(line)}</div>`;
});
return html;
}
/* -------------------------
COMMIT SEARCH
------------------------- */
async function handleCommitSearch(query) {
if (!query || query.trim().length === 0) {
renderCommitTimeline(currentCommitView.commits);
return;
}
setStatus('Searching commits...');
try {
const res = await window.electronAPI.searchCommits({
owner: currentCommitView.owner,
repo: currentCommitView.repo,
branch: currentCommitView.branch,
query: query.trim()
});
if (res.ok) {
renderCommitTimeline(res.commits);
setStatus(`Found ${res.commits.length} commits`);
} else {
setStatus('Search failed');
}
} catch (error) {
console.error('Search error:', error);
setStatus('Search error');
}
}
/* -------------------------
HELPER FUNCTIONS
------------------------- */
function formatRelativeTime(date) {
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'just now';
if (diffMins < 60) return `${diffMins} min ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) > 1 ? 's' : ''} ago`;
if (diffDays < 365) return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) > 1 ? 's' : ''} ago`;
return `${Math.floor(diffDays / 365)} year${Math.floor(diffDays / 365) > 1 ? 's' : ''} ago`;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/* -------------------------
EVENT LISTENERS
------------------------- */
// File Editor Event Listeners
setTimeout(() => {
// Buttons
const btnClose = $('btnCloseEditor');
const btnSave = $('btnEditorSave');
const btnSearch = $('btnEditorSearch');
const btnDiscard = $('btnDiscardEdit');
const btnFileActions = $('btnFileActions');
const modal = $('fileEditorModal');
// Close button
if (btnClose) btnClose.addEventListener('click', closeFileEditor);
// Save button
if (btnSave) btnSave.addEventListener('click', saveCurrentFile);
// Search button
if (btnSearch) btnSearch.addEventListener('click', toggleSearch);
// Discard button
if (btnDiscard) btnDiscard.addEventListener('click', closeFileEditor);
// File actions menu
if (btnFileActions) {
btnFileActions.addEventListener('click', (e) => {
const menu = $('fileActionsMenu');
if (menu) {
menu.classList.toggle('hidden');
const rect = btnFileActions.getBoundingClientRect();
menu.style.top = (rect.bottom + 4) + 'px';
menu.style.right = '20px';
}
});
}
// Search & Replace
const searchInput = $('searchInput');
if (searchInput) {
searchInput.addEventListener('keyup', performSearch);
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
replaceOnce();
}
});
}
const btnReplace = $('btnReplace');
if (btnReplace) btnReplace.addEventListener('click', replaceOnce);
const btnReplaceAll = $('btnReplaceAll');
if (btnReplaceAll) btnReplaceAll.addEventListener('click', replaceAll);
const btnCloseSearch = $('btnCloseSearch');
if (btnCloseSearch) btnCloseSearch.addEventListener('click', () => {
const searchBar = $('searchBar');
if (searchBar) searchBar.classList.add('hidden');
});
// Textarea events
const textarea = $('fileEditorContent');
if (textarea) {
textarea.addEventListener('input', () => {
updateEditorContent(textarea.value);
});
textarea.addEventListener('scroll', () => {
const lineNumbers = $('lineNumbers');
if (lineNumbers) lineNumbers.scrollTop = textarea.scrollTop;
});
textarea.addEventListener('click', updateEditorStats);
textarea.addEventListener('keyup', updateEditorStats);
}
// Close modal on background click
if (modal) {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeFileEditor();
}
});
}
console.log('✅ Advanced editor event listeners registered');
}, 100);
// Global keyboard shortcuts
document.addEventListener('keydown', (e) => {
const modal = $('fileEditorModal');
if (!modal || modal.classList.contains('hidden')) return;
// Ctrl+S / Cmd+S - Save
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveCurrentFile();
}
// Ctrl+F / Cmd+F - Search
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
toggleSearch();
}
// Ctrl+H / Cmd+H - Replace
if ((e.ctrlKey || e.metaKey) && e.key === 'h') {
e.preventDefault();
toggleSearch();
$('replaceInput').focus();
}
// ESC - Close search bar if open
if (e.key === 'Escape') {
const searchBar = $('searchBar');
if (searchBar && !searchBar.classList.contains('hidden')) {
searchBar.classList.add('hidden');
}
}
});
/* ========================================
UPDATER FUNKTIONEN (Optimiert & Synchronisiert)
======================================== */
async function initUpdater() {
try {
const versionRes = await window.electronAPI.getAppVersion();
if (versionRes && versionRes.ok && $('appVersion')) {
$('appVersion').value = versionRes.version;
}
} catch (error) {
console.error('[Renderer] Fehler beim Laden der Version:', error);
}
// Manueller Check Button in Settings
if ($('btnCheckUpdates')) {
$('btnCheckUpdates').onclick = async () => {
const btn = $('btnCheckUpdates');
const originalHTML = btn.innerHTML;
btn.innerHTML = '⏳ Suche...';
btn.disabled = true;
try {
await window.electronAPI.checkForUpdates();
setStatus('Update-Suche abgeschlossen');
} catch (error) {
setStatus('Fehler bei der Update-Prüfung');
} finally {
setTimeout(() => {
btn.innerHTML = originalHTML;
btn.disabled = false;
}, 1500);
}
};
}
}
// Event-Listener für das Update-Modal
if (window.electronAPI.onUpdateAvailable) {
window.electronAPI.onUpdateAvailable((info) => {
const modal = $('updateModal');
const versionInfo = $('updateVersionInfo');
const changelog = $('updateChangelog');
if (versionInfo) versionInfo.innerText = `Version ${info.version} verfügbar!`;
if (changelog) changelog.innerText = info.body || 'Keine Release-Notes vorhanden.';
if (modal) modal.classList.remove('hidden');
// Button: Jetzt installieren
const updateBtn = $('btnStartUpdate');
if (updateBtn) {
updateBtn.onclick = () => {
if (modal) modal.classList.add('hidden');
setStatus('Download gestartet...');
// Aufruf der korrekten Preload-Funktion
window.electronAPI.startUpdateDownload(info.asset);
};
}
// Button: Später
const ignoreBtn = $('btnIgnoreUpdate');
if (ignoreBtn) {
ignoreBtn.onclick = () => { if (modal) modal.classList.add('hidden'); };
}
});
}
// AM ENDE DER DATEI: Initialisierung beim Start
document.addEventListener('DOMContentLoaded', () => {
// 1. Basis-Setup (Settings-Feld füllen etc.)
initUpdater();
// 2. AUTOMATISCHER UPDATE-CHECK BEIM START
// Wir warten 3 Sekunden, damit die App in Ruhe laden kann
setTimeout(() => {
console.log("[Auto-Updater] Suche im Hintergrund nach Updates...");
window.electronAPI.checkForUpdates();
}, 3000);
});