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

3195 lines
104 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 and auto-login if available
try {
const creds = await window.electronAPI.loadCredentials();
if (creds) {
// Fülle Settings-Felder
if ($('githubToken')) $('githubToken').value = creds.githubToken || '';
if ($('giteaToken')) $('giteaToken').value = creds.giteaToken || '';
if ($('giteaURL')) $('giteaURL').value = creds.giteaURL || '';
// 🆕 AUTO-LOGIN: Wenn Gitea-Credentials vorhanden sind, lade sofort die Repos
if (creds.giteaToken && creds.giteaURL) {
console.log('✅ Credentials gefunden - Auto-Login wird gestartet...');
setStatus('Lade deine Projekte...');
// Kurze Verzögerung damit UI fertig geladen ist
setTimeout(() => {
loadGiteaRepos();
}, 500);
} else {
console.log(' Keine vollständigen Gitea-Credentials - bitte in Settings eintragen');
setStatus('Bereit - bitte Settings konfigurieren');
}
} else {
console.log(' Keine Credentials gespeichert');
setStatus('Bereit - bitte Settings konfigurieren');
}
} catch (error) {
console.error('Error loading credentials:', error);
setStatus('Fehler beim Laden der Einstellungen');
}
// Rest of Event Handlers... (bleibt unverändert)
// Event Handlers
if ($('btnLoadGiteaRepos')) {
$('btnLoadGiteaRepos').onclick = loadGiteaRepos;
}
if ($('btnSelectFolder')) {
$('btnSelectFolder').onclick = selectLocalFolder;
}
if ($('btnPush')) {
$('btnPush').onclick = pushLocalFolder;
}
if ($('btnCreateRepo')) {
$('btnCreateRepo').onclick = createRepoHandler;
}
if ($('btnBack')) {
$('btnBack').onclick = () => {
if (currentState.view === 'gitea-repo') {
if (currentState.path === '' || currentState.path === '/') {
loadGiteaRepos();
} else {
const parts = currentState.path.split('/').filter(p => p);
parts.pop();
loadRepoContents(currentState.owner, currentState.repo, parts.join('/'));
}
}
};
}
// Modal controls
if ($('btnSettings')) {
$('btnSettings').onclick = () => {
$('settingsModal').classList.remove('hidden');
};
}
if ($('btnCloseSettings')) {
$('btnCloseSettings').onclick = () => {
$('settingsModal').classList.add('hidden');
};
}
if ($('btnOpenRepoActions')) {
$('btnOpenRepoActions').onclick = () => {
$('repoActionModal').classList.remove('hidden');
};
}
if ($('btnCloseRepoActions')) {
$('btnCloseRepoActions').onclick = () => {
$('repoActionModal').classList.add('hidden');
};
}
if ($('btnSaveSettings')) {
$('btnSaveSettings').onclick = async () => {
try {
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();
}
};
}
// 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>
`;
// Event-Listener MUSS VOR innerHTML += gesetzt werden
const newBtn = grid.querySelector('.btn-new-release');
if (newBtn) {
newBtn.onclick = () => {
console.log('New Release button clicked');
showCreateReleaseModal(owner, repo);
};
} else {
console.error('New Release button not found in DOM');
}
if (!res.releases || res.releases.length === 0) {
// WICHTIG: appendChild statt innerHTML +=, um Event-Listener zu erhalten
const emptyMsg = document.createElement('div');
emptyMsg.style.cssText = 'grid-column: 1/-1; text-align: center; padding: 60px; color: var(--text-muted); font-size: 16px;';
emptyMsg.textContent = '📭 Noch keine Releases veröffentlicht';
grid.appendChild(emptyMsg);
setStatus('No releases');
return;
}
// Releases als Cards darstellen
res.releases.forEach((release, index) => {
const card = createReleaseCard(release, index === 0);
grid.appendChild(card);
});
setStatus(`${res.releases.length} release(s) loaded`);
} catch (error) {
console.error('Error loading releases:', error);
setStatus('Failed to load releases');
}
}
function createReleaseCard(release, isLatest) {
const card = document.createElement('div');
card.className = 'release-card';
card.style.cssText = `
grid-column: 1/-1;
background: var(--bg-tertiary);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-lg);
transition: all var(--transition-normal);
`;
// Header mit Tag und Badges
const header = document.createElement('div');
header.style.cssText = 'display: flex; gap: 10px; align-items: center; margin-bottom: 12px;';
const tag = document.createElement('span');
tag.textContent = release.tag_name;
tag.style.cssText = `
background: var(--accent-gradient);
color: #000;
padding: 6px 16px;
border-radius: 20px;
font-weight: 700;
font-size: 14px;
`;
header.appendChild(tag);
if (isLatest) {
const latestBadge = document.createElement('span');
latestBadge.textContent = 'LATEST';
latestBadge.style.cssText = `
background: var(--success);
color: #000;
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 700;
`;
header.appendChild(latestBadge);
}
if (release.prerelease) {
const preBadge = document.createElement('span');
preBadge.textContent = 'PRE-RELEASE';
preBadge.style.cssText = `
background: var(--warning);
color: #000;
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 700;
`;
header.appendChild(preBadge);
}
if (release.draft) {
const draftBadge = document.createElement('span');
draftBadge.textContent = 'DRAFT';
draftBadge.style.cssText = `
background: rgba(255, 255, 255, 0.2);
color: var(--text-primary);
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 700;
`;
header.appendChild(draftBadge);
}
card.appendChild(header);
// Title
const title = document.createElement('h3');
title.textContent = release.name || release.tag_name;
title.style.cssText = 'margin: 0 0 12px 0; color: var(--text-primary); font-size: 20px;';
card.appendChild(title);
// Body (Release Notes)
if (release.body) {
const body = document.createElement('div');
body.className = 'release-body';
body.innerHTML = parseMarkdownToHTML(release.body);
card.appendChild(body);
}
// Assets
if (release.assets && release.assets.length > 0) {
const assetsContainer = document.createElement('div');
assetsContainer.style.cssText = `
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
`;
const assetsTitle = document.createElement('div');
assetsTitle.textContent = '📦 Assets';
assetsTitle.style.cssText = 'font-weight: 600; margin-bottom: 12px; color: var(--text-primary);';
assetsContainer.appendChild(assetsTitle);
release.assets.forEach(asset => {
const assetItem = document.createElement('div');
assetItem.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background: rgba(255, 255, 255, 0.03);
border-radius: var(--radius-sm);
margin-bottom: 8px;
`;
const assetName = document.createElement('span');
assetName.textContent = `📎 ${asset.name}`;
assetName.style.cssText = 'color: var(--text-primary);';
const assetSize = document.createElement('span');
assetSize.textContent = formatBytes(asset.size || 0);
assetSize.style.cssText = 'color: var(--text-muted); font-size: 12px; margin-left: 12px;';
const downloadBtn = document.createElement('button');
downloadBtn.textContent = '⬇️ Download';
downloadBtn.style.cssText = `
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 6px 12px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 12px;
`;
downloadBtn.onclick = () => {
if (asset.browser_download_url) {
window.open(asset.browser_download_url, '_blank');
}
};
const deleteAssetBtn = document.createElement('button');
deleteAssetBtn.textContent = '🗑️';
deleteAssetBtn.style.cssText = `
background: transparent;
color: var(--danger);
border: 1px solid var(--danger);
padding: 6px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 12px;
margin-left: 8px;
`;
deleteAssetBtn.onclick = async () => {
if (confirm(`Delete asset "${asset.name}"?`)) {
const res = await window.electronAPI.deleteReleaseAsset({
owner: currentReleaseView.owner,
repo: currentReleaseView.repo,
assetId: asset.id
});
if (res.ok) {
assetItem.remove();
setStatus('Asset deleted');
}
}
};
const leftSide = document.createElement('div');
leftSide.style.cssText = 'display: flex; align-items: center; gap: 12px;';
leftSide.appendChild(assetName);
leftSide.appendChild(assetSize);
const rightSide = document.createElement('div');
rightSide.style.cssText = 'display: flex; gap: 8px;';
rightSide.appendChild(downloadBtn);
rightSide.appendChild(deleteAssetBtn);
assetItem.appendChild(leftSide);
assetItem.appendChild(rightSide);
assetsContainer.appendChild(assetItem);
});
card.appendChild(assetsContainer);
}
// Meta Info
const meta = document.createElement('div');
meta.style.cssText = `
display: flex;
gap: 20px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
color: var(--text-muted);
font-size: 12px;
`;
const date = new Date(release.created_at);
const dateStr = date.toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
meta.innerHTML = `
<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 (MIT DATEI-UPLOAD)
------------------------- */
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>
<!-- NEU: Datei Upload -->
<div class="input-group">
<label>Release Asset (Optional)</label>
<div style="display: flex; gap: 10px;">
<input id="releaseAssetInput" type="text" readonly placeholder="Keine Datei gewählt" style="
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;
">
<button id="btnSelectReleaseAsset" type="button" style="
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;
">📎 Datei wählen</button>
</div>
</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);
// Variable zum Speichern des gewählten Dateipfads
let selectedAssetPath = null;
// Event Listener: Datei auswählen
$('btnSelectReleaseAsset').onclick = async () => {
try {
const res = await window.electronAPI.selectFile();
if (res.ok && res.files && res.files.length > 0) {
selectedAssetPath = res.files[0];
const fileName = selectedAssetPath.split(/[\\/]/).pop();
$('releaseAssetInput').value = fileName;
$('releaseAssetInput').style.color = 'var(--text-primary)';
$('releaseAssetInput').style.borderColor = 'var(--accent-primary)';
}
} catch (error) {
console.error('Fehler beim Auswählen der Datei:', error);
alert('Konnte Dateidialog nicht öffnen.');
}
};
// Event Listener: Release erstellen
$('btnCreateRelease').onclick = async () => {
const tag = $('releaseTag').value.trim();
const name = $('releaseName').value.trim() || tag;
const body = $('releaseBody').value.trim();
const target = $('releaseTarget').value.trim() || 'main';
const prerelease = $('releasePrerelease').checked;
const draft = $('releaseDraft').checked;
if (!tag) {
alert('Tag Version ist erforderlich!');
return;
}
setStatus('Creating release...');
try {
// 1. Release erstellen
const res = await window.electronAPI.createRelease({
owner,
repo,
tag_name: tag,
name,
body,
target_commitish: target,
prerelease,
draft
});
if (res.ok) {
// 2. Falls eine Datei ausgewählt wurde, direkt hochladen
if (selectedAssetPath) {
setStatus('Release erstellt. Lade Datei hoch...');
showProgress(50, 'Uploading Asset...');
try {
const fileName = selectedAssetPath.split(/[\\/]/).pop();
const uploadRes = await window.electronAPI.uploadReleaseAsset({
owner,
repo,
releaseId: res.release.id,
filePath: selectedAssetPath,
fileName
});
if (uploadRes.ok) {
setStatus(`Release "${tag}" und Asset erstellt!`);
} else {
console.error('Asset Upload fehlgeschlagen:', uploadRes.error);
alert(`Release erstellt, aber Asset Upload fehlgeschlagen: ${uploadRes.error}`);
setStatus('Release erstellt (Upload Fehler)');
}
} catch (uploadErr) {
console.error('Upload error:', uploadErr);
alert('Release erstellt, aber Fehler beim Hochladen der Datei.');
setStatus('Release erstellt (Upload Fehler)');
} finally {
hideProgress();
}
} else {
setStatus('Release created!');
}
modal.remove();
loadRepoReleases(owner, repo); // Liste neu laden
} else {
setStatus('Failed: ' + res.error);
alert('Fehler beim Erstellen des Releases: ' + res.error);
}
} catch (error) {
console.error('Create release error:', error);
setStatus('Create failed');
alert('Ein unerwarteter Fehler ist aufgetreten.');
}
};
$('btnCancelRelease').onclick = () => modal.remove();
// Close on background click
modal.onclick = (e) => {
if (e.target === modal) modal.remove();
};
}
/* -------------------------
UPLOAD ASSET DIALOG
------------------------- */
async function showUploadAssetDialog(release) {
try {
const res = await window.electronAPI.selectFile();
if (!res.ok || !res.files || res.files.length === 0) {
return;
}
const filePath = res.files[0];
const fileName = filePath.split(/[\\/]/).pop();
setStatus(`Uploading ${fileName}...`);
showProgress(0, `Uploading ${fileName}...`);
const uploadRes = await window.electronAPI.uploadReleaseAsset({
owner: currentReleaseView.owner,
repo: currentReleaseView.repo,
releaseId: release.id,
filePath,
fileName
});
hideProgress();
if (uploadRes.ok) {
setStatus('Asset uploaded!');
// Reload releases to show new asset
loadRepoReleases(currentReleaseView.owner, currentReleaseView.repo);
} else {
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);
});