From fd868ea238cc7c3cf81fc73b6db5193fc9010062 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Mon, 2 Feb 2026 17:55:31 +0100 Subject: [PATCH] Update from Git Manager GUI --- renderer/index.html | 214 ++- renderer/renderer.js | 3445 +++++++++++++++++++++++++++++++++++------- renderer/style.css | 1403 ++++++++++++++++- 3 files changed, 4407 insertions(+), 655 deletions(-) diff --git a/renderer/index.html b/renderer/index.html index cf0be02..a632886 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -1,89 +1,169 @@ - + - Git Manager Explorer + Git Manager Explorer Pro
+
- - - - - +
+ + + + +
+ +
+ + + + + +
+ + Bereit
-
- - -
-
-
-
- - -
- - -
- - -
- -
- -
-
-
-
-
- -
-

Preview

-
Select a file to preview
-
- -
-

Commit Logs

-
No commits yet.
-
-
-
+ +
+
+ +
+
- + \ No newline at end of file diff --git a/renderer/renderer.js b/renderer/renderer.js index 891e681..733cc26 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -1,621 +1,2960 @@ -// renderer.js — Explorer with repo drag-export and folder upload+git push + progress UI +// renderer.js — Grid-UI + Navigation + Drag'n'Drop mit Fehlerbehandlung const $ = id => document.getElementById(id); let selectedFolder = null; -let giteaCache = {}; // cache repo contents +let giteaCache = {}; -function setStatus(txt) { const s = $('status'); if (s) s.innerText = txt || ''; } +// 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 || ''; +} /* ------------------------- - Small dynamic progress UI (created if not present) + PROGRESS UI ------------------------- */ function ensureProgressUI() { - if (document.getElementById('folderProgressContainer')) return; - const container = document.createElement('div'); - container.id = 'folderProgressContainer'; - container.style.position = 'fixed'; - container.style.left = '50%'; - container.style.top = '12px'; - container.style.transform = 'translateX(-50%)'; - container.style.zIndex = '10000'; - container.style.width = '480px'; - container.style.maxWidth = '90%'; - container.style.padding = '6px'; - container.style.background = 'rgba(20,20,30,0.95)'; - container.style.borderRadius = '8px'; - container.style.boxShadow = '0 6px 18px rgba(0,0,0,0.45)'; - container.style.color = '#fff'; - container.style.fontFamily = 'sans-serif'; - container.style.display = 'none'; + 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.marginBottom = '6px'; - text.style.fontSize = '13px'; - container.appendChild(text); + 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.width = '100%'; - barWrap.style.height = '10px'; - barWrap.style.background = '#333'; - barWrap.style.borderRadius = '6px'; - barWrap.style.overflow = 'hidden'; + 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.width = '0%'; - bar.style.height = '100%'; - bar.style.background = '#4caf50'; - bar.style.transition = 'width 150ms linear'; + 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); + barWrap.appendChild(bar); + container.appendChild(barWrap); + document.body.appendChild(container); } function showProgress(percent, text) { - ensureProgressUI(); - const container = document.getElementById('folderProgressContainer'); - const bar = document.getElementById('folderProgressBar'); - const txt = document.getElementById('folderProgressText'); - txt.innerText = text || ''; - bar.style.width = `${percent}%`; - container.style.display = 'block'; + 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 = document.getElementById('folderProgressContainer'); - if (container) container.style.display = 'none'; + const container = $('folderProgressContainer'); + if (container) { + setTimeout(() => { + container.style.display = 'none'; + }, 500); + } } /* ------------------------- - Initialization & wiring + ADVANCED FILE EDITOR - WITH TABS, UNDO/REDO, AUTO-SAVE, LINE NUMBERS ------------------------- */ -async function init() { - 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 || ''; - } - if ($('btnLoadGiteaRepos')) $('btnLoadGiteaRepos').addEventListener('click', loadGiteaRepos); - if ($('btnSelectFolder')) $('btnSelectFolder').addEventListener('click', selectLocalFolder); - if ($('btnPush')) $('btnPush').addEventListener('click', pushLocalFolder); - if ($('btnCreateRepo')) $('btnCreateRepo').addEventListener('click', createRepoHandler); +// Editor State +let openTabs = {}; // { filePath: { name, content, originalContent, dirty, icon, history, historyIndex } } +let currentActiveTab = null; +let autoSaveTimer = null; +let autoSaveInterval = 3000; // 3 sekunden - if ($('btnSettings')) $('btnSettings').addEventListener('click', () => $('settingsModal').classList.remove('hidden')); - if ($('btnCloseSettings')) $('btnCloseSettings').addEventListener('click', () => $('settingsModal').classList.add('hidden')); - if ($('btnSaveSettings')) $('btnSaveSettings').addEventListener('click', saveSettings); +// 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(); + } + }); +} - // global drag-over/drop: prevent default - document.addEventListener('dragover', e => e.preventDefault()); - document.addEventListener('drop', e => e.preventDefault()); +// 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 +} - // subscribe to progress events - window.electronAPI.onFolderUploadProgress((payload) => { - const { processed, total, percent } = payload; - showProgress(percent, `Upload: ${processed}/${total} (${percent}%)`); - if (processed >= total) setTimeout(hideProgress, 600); - }); - window.electronAPI.onFolderDownloadProgress((payload) => { - const { processed, total, percent } = payload; - showProgress(percent, `Download: ${processed}/${total} (${percent}%)`); - if (processed >= total) setTimeout(hideProgress, 600); - }); +// 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(); + } +} - window.electronAPI.onPushProgress(p => setStatus('Pushing... ' + p + '%')); - ensureProgressUI(); +// 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 = '
Fehler beim Laden des Bildes
'; + return; + } + } else { + // Lokale Datei + imgSrc = 'file:///' + currentActiveTab.replace(/\\/g, '/'); + } + + imagePreview.innerHTML = `${tab.name}Bild konnte nicht geladen werden
';">`; + } + } 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] || '📄'; } /* ------------------------- - Settings / buttons + SEARCH & REPLACE ------------------------- */ -async function saveSettings() { - const data = { githubToken: $('githubToken')?.value, giteaToken: $('giteaToken')?.value, giteaURL: $('giteaURL')?.value }; - const r = await window.electronAPI.saveCredentials(data); - setStatus(r.ok ? 'Settings saved' : 'Save failed: ' + r.error); - $('settingsModal').classList.add('hidden'); +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, '&') + .replace(//g, '>'); + } + + // Convert markdown patterns + // Headings: ### Title →

Title

+ html = html.replace(/^###### (.*?)$/gm, '
$1
'); + html = html.replace(/^##### (.*?)$/gm, '
$1
'); + html = html.replace(/^#### (.*?)$/gm, '

$1

'); + html = html.replace(/^### (.*?)$/gm, '

$1

'); + html = html.replace(/^## (.*?)$/gm, '

$1

'); + html = html.replace(/^# (.*?)$/gm, '

$1

'); + + // Horizontal rule: --- or *** or ___ + html = html.replace(/^\-{3,}$/gm, '
'); + html = html.replace(/^\*{3,}$/gm, '
'); + html = html.replace(/^_{3,}$/gm, '
'); + + // Bold: **text** or __text__ + html = html.replace(/\*\*(.*?)\*\*/g, '$1'); + html = html.replace(/__(.*?)__/g, '$1'); + + // Italic: *text* or _text_ (but not within words) + html = html.replace(/\s\*(.*?)\*\s/g, ' $1 '); + html = html.replace(/\s_(.*?)_\s/g, ' $1 '); + + // Convert line breaks to
+ html = html.replace(/\n/g, '
'); + + // Wrap plain text in paragraphs (text not already in tags) + let lines = html.split('
'); + lines = lines.map(line => { + line = line.trim(); + if (line && !line.match(/^ 0) { + // Only wrap if not already a tag + if (!line.match(/^<(h[1-6]|hr|p|div|ul|ol|li|em|strong|b|i)/)) { + return '

' + line + '

'; + } + } + return line; + }); + + html = lines.join('
'); + + return html; +} + +/* ------------------------- + NAVIGATION & UI UPDATES + ------------------------- */ +function updateNavigationUI() { + const btnBack = $('btnBack'); + if (!btnBack) return; + + // Back Button zeigen, wenn wir in einem Repo oder tief in Ordnern sind + if (currentState.view === 'gitea-repo' || + (currentState.view === 'gitea-list' && currentState.path !== '')) { + btnBack.classList.remove('hidden'); + } else { + btnBack.classList.add('hidden'); + } +} + +/* ------------------------- + GITEA CORE LOGIK (GRID) + ------------------------- */ async function loadGiteaRepos() { - setStatus('Loading Gitea repos...'); - const creds = await window.electronAPI.loadCredentials(); - if (!creds || !creds.giteaToken || !creds.giteaURL) { setStatus('Set Gitea token & URL in Settings'); return; } - const res = await window.electronAPI.listGiteaRepos({ token: creds.giteaToken, url: creds.giteaURL }); - if (!res.ok) { setStatus('Failed to load repos: ' + res.error); return; } - renderGiteaRoots(res.repos); - setStatus(`Loaded ${res.repos.length} repos`); + 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 = '
Keine Repositories gefunden
'; + setStatus('No repositories found'); + return; + } + + // --- NEU: Suchfeld für Projekte --- + const searchContainer = document.createElement('div'); + searchContainer.style.cssText = 'grid-column: 1/-1; margin-bottom: 20px;'; + + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.placeholder = '🔍 Projekt suchen...'; + searchInput.style.cssText = ` + width: 100%; + padding: 12px 16px; + border-radius: var(--radius-md); + border: 1px solid rgba(255, 255, 255, 0.1); + background: var(--bg-tertiary); + color: var(--text-primary); + font-size: 14px; + outline: none; + box-sizing: border-box; + `; + + // Search Focus Effekt + searchInput.addEventListener('focus', () => { + searchInput.style.borderColor = 'var(--accent-primary)'; + }); + searchInput.addEventListener('blur', () => { + searchInput.style.borderColor = 'rgba(255, 255, 255, 0.1)'; + }); + + searchContainer.appendChild(searchInput); + grid.appendChild(searchContainer); + + // Search Logic + searchInput.addEventListener('input', (e) => { + const val = e.target.value.toLowerCase(); + const cards = grid.querySelectorAll('.item-card'); + cards.forEach(card => { + const name = card.querySelector('.item-name').textContent.toLowerCase(); + if (name.includes(val)) { + card.style.display = 'flex'; + } else { + card.style.display = 'none'; + } + }); + }); + // ----------------------------------- + + 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 = `
📦
${repoName}
`; + 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 selectLocalFolder() { - const folder = await window.electronAPI.selectFolder(); - if (!folder) return; - selectedFolder = folder; - setStatus('Selected local folder: ' + folder); - await refreshLocalTree(folder); - await loadBranches(folder); - await loadCommitLogs(folder); -} - -async function pushLocalFolder() { - if (!selectedFolder) return alert('Select local folder first'); - const branch = $('branchSelect')?.value || 'main'; - setStatus('Pushing...'); - const res = await window.electronAPI.pushProject({ folder: selectedFolder, branch, repoName: $('repoName')?.value, platform: $('platform')?.value }); - setStatus(res.ok ? 'Push succeeded' : 'Push failed: ' + res.error); - if (res.ok) loadCommitLogs(selectedFolder); -} - -async function createRepoHandler() { - const name = $('repoName')?.value?.trim(); - const platform = $('platform')?.value; - const license = $('licenseSelect')?.value || ''; - const autoInit = $('createReadme')?.checked; - - if (!name) return alert('Repo name required'); - setStatus('Creating repo...'); - const res = await window.electronAPI.createRepo({ name, platform, license, autoInit }); - setStatus(res.ok ? 'Repo created' : 'Create failed: ' + res.error); -} - -/* ------------------------- - Render Gitea roots + draggable/droppable nodes - ------------------------- */ -function renderGiteaRoots(repos) { - const tree = $('fileTree'); - const prev = tree.querySelector('[data-is-gitea-root="1"]'); - if (prev) { - const next = prev.nextSibling; - prev.remove(); - if (next && next.classList && next.classList.contains('gitea-children-wrapper')) next.remove(); - } - - const groot = document.createElement('div'); - groot.className = 'file-node folder'; - groot.dataset.isGiteaRoot = '1'; - groot.innerText = 'Gitea Repositories'; - groot.style.fontWeight = '800'; - const wrapper = document.createElement('div'); - wrapper.className = 'gitea-children-wrapper'; - wrapper.style.marginLeft = '6px'; - - repos.forEach(repo => { - let owner = (repo.owner && (repo.owner.login || repo.owner.username)) || null; - let repoName = repo.name; - if (!owner && repo.full_name && repo.full_name.includes('/')) { - const parts = repo.full_name.split('/'); - owner = parts[0]; repoName = parts[1]; +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); } - const rdiv = document.createElement('div'); - rdiv.className = 'file-node folder'; - rdiv.innerText = (repo.full_name || `${owner}/${repoName}`); - rdiv.dataset.giteaOwner = owner; - rdiv.dataset.giteaRepo = repoName; - rdiv.dataset.giteaClone = repo.clone_url || repo.clone_url_ssh || (repo.html_url ? `${repo.html_url}.git` : ''); - rdiv.style.paddingLeft = '8px'; - rdiv.draggable = true; + // WICHTIG: Grid-Layout zurücksetzen + const grid = $('explorerGrid'); + if (grid) { + grid.style.gridTemplateColumns = ''; + } - // dragstart - rdiv.addEventListener('dragstart', async (ev) => { - ev.preventDefault(); - setStatus(`Preparing download for ${owner}/${repoName} ...`); - showProgress(0, `Preparing ${owner}/${repoName}...`); - const res = await window.electronAPI.prepareDownloadDrag({ owner, repo: repoName, path: '' }); - if (!res.ok) { setStatus('Prepare failed: ' + res.error); hideProgress(); return; } - const tempPath = res.tempPath; - window.electronAPI.startNativeDrag(tempPath); - setStatus('Drag started — drop to desktop or file manager.'); - hideProgress(); - }); + 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; + } - // drop - rdiv.addEventListener('dragover', (ev) => { ev.preventDefault(); rdiv.classList.add('drag-target'); }); - rdiv.addEventListener('dragleave', () => { rdiv.classList.remove('drag-target'); }); + const grid = $('explorerGrid'); + if (!grid) return; + grid.innerHTML = ''; - rdiv.addEventListener('drop', async (ev) => { - ev.preventDefault(); - rdiv.classList.remove('drag-target'); - const dt = ev.dataTransfer; - if (!dt || !dt.files || dt.files.length === 0) { - setStatus('No files/folders dropped.'); + if (!res.items || res.items.length === 0) { + grid.innerHTML = '
Leerer Ordner
'; + setStatus('Empty folder'); + return; + } + + res.items.forEach(item => { + const icon = item.type === 'dir' ? '📁' : '📄'; + const card = document.createElement('div'); + card.className = 'item-card'; + card.innerHTML = `
${icon}
${item.name}
`; + + // 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 = '
Keine Dateien gefunden
'; + return; + } + + res.tree.forEach(node => { + const card = document.createElement('div'); + card.className = 'item-card'; + card.innerHTML = ` +
${node.isDirectory ? '📁' : '📄'}
+
${node.name}
+ `; + + 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 paths = Array.from(dt.files).map(f => f.path); - for (const p of paths) { - setStatus(`Uploading ${p} to ${owner}/${repoName} ...`); - showProgress(0, `Uploading ${ppathBasename(p)} → ${owner}/${repoName}`); - const res = await window.electronAPI.uploadAndPush({ localFolder: p, owner, repo: repoName, destPath: '', cloneUrl: rdiv.dataset.giteaClone, branch: 'main' }); - if (!res.ok) setStatus('Upload failed: ' + res.error); - else setStatus(res.usedGit ? `Upload+Push OK (git used)` : `Upload OK (API used)`); + } + + 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(); - } - try { await expandGiteaDir(rdiv, owner, repoName, rdiv.dataset.giteaPath || ''); } catch (_) {} - }); + } +} - // Click - FIXED: ref: 'main' explicitly added - rdiv.addEventListener('click', async (ev) => { - ev.stopPropagation(); - if (rdiv._expanded) { - const next = rdiv.nextSibling; - if (next && next.classList && next.classList.contains('gitea-children')) next.remove(); - rdiv._expanded = false; - return; - } - setStatus(`Loading ${owner}/${repoName}...`); - const res = await window.electronAPI.getGiteaRepoContents({ owner, repo: repoName, path: '', ref: 'main' }); - if (!res.ok) { setStatus('Failed to load repo: ' + res.error); return; } - const cont = document.createElement('div'); - cont.className = 'gitea-children'; - cont.style.marginLeft = '12px'; - (res.items || []).forEach(item => { - const node = document.createElement('div'); - node.className = 'file-node ' + (item.type === 'dir' ? 'folder' : 'file'); - node.innerText = item.name; - node.dataset.giteaOwner = owner; - node.dataset.giteaRepo = repoName; - node.dataset.giteaPath = item.path; - node.style.paddingLeft = '8px'; +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); + } +} - node.addEventListener('dragover', e => e.preventDefault()); - node.addEventListener('drop', async (e) => { - e.preventDefault(); - const dt = e.dataTransfer; - if (!dt || !dt.files || dt.files.length === 0) { setStatus('No files dropped'); return; } - const dropped = Array.from(dt.files).map(f => f.path); - for (const p of dropped) { - setStatus(`Uploading ${p} to ${owner}/${repoName}/${item.path} ...`); - showProgress(0, `Uploading ${ppathBasename(p)} → ${owner}/${repoName}/${item.path}`); - const res = await window.electronAPI.uploadAndPush({ localFolder: p, owner, repo: repoName, destPath: item.path, cloneUrl: rdiv.dataset.giteaClone, branch: 'main' }); - if (!res.ok) setStatus('Upload failed: ' + res.error); - else setStatus(res.usedGit ? `Upload+Push OK (git used)` : `Upload OK (API used)`); +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(); - } - await expandGiteaDir(node, owner, repoName, item.path); - }); - - node.addEventListener('click', async (e) => { - e.stopPropagation(); - if (item.type === 'dir') await expandGiteaDir(node, owner, repoName, item.path); - else await previewGiteaFile(owner, repoName, item.path); - document.querySelectorAll('.file-node').forEach(n => n.classList.remove('active')); - node.classList.add('active'); - }); - - node.addEventListener('contextmenu', (ev) => { - ev.preventDefault(); - showGiteaContextMenu(node, ev.clientX, ev.clientY); - }); - - cont.appendChild(node); - }); - rdiv.after(cont); - rdiv._expanded = true; - setStatus(''); + setStatus('Upload failed'); + } }); - // Context menu for Repo - rdiv.addEventListener('contextmenu', (ev) => { - ev.preventDefault(); - showRepoContextMenu(rdiv, ev.clientX, ev.clientY, owner, repoName, rdiv.dataset.giteaClone); - }); + 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'); - wrapper.appendChild(rdiv); - }); - - tree.prepend(groot); - groot.after(wrapper); + 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; } -} - -/* Expand dir / render children / preview */ -async function expandGiteaDir(nodeEl, owner, repo, dirPath) { - if (nodeEl._expanded) { - const next = nodeEl.nextSibling; - if (next && next.classList && next.classList.contains('gitea-sub')) next.remove(); - nodeEl._expanded = false; - return; - } - const key = `${owner}/${repo}:${dirPath}`; - const ref = 'main'; // FIXED: Always use 'main' - if (giteaCache[key]) { renderGiteaChildren(nodeEl, giteaCache[key], owner, repo); nodeEl._expanded = true; return; } - setStatus(`Loading ${dirPath}...`); - const res = await window.electronAPI.getGiteaRepoContents({ owner, repo, path: dirPath, ref }); - if (!res.ok) { setStatus('Failed: ' + res.error); return; } - giteaCache[key] = res.items; - renderGiteaChildren(nodeEl, res.items, owner, repo); - nodeEl._expanded = true; - setStatus(''); -} - -function renderGiteaChildren(parentNode, items, owner, repo) { - const container = document.createElement('div'); - container.className = 'gitea-sub'; - container.style.marginLeft = '12px'; - items.forEach(item => { - const el = document.createElement('div'); - el.className = 'file-node ' + (item.type === 'dir' ? 'folder' : 'file'); - el.innerText = item.name; - el.dataset.giteaOwner = owner; - el.dataset.giteaRepo = repo; - el.dataset.giteaPath = item.path; - el.style.paddingLeft = '8px'; - el.addEventListener('click', async (ev) => { - ev.stopPropagation(); - if (item.type === 'dir') await expandGiteaDir(el, owner, repo, item.path); - else await previewGiteaFile(owner, repo, item.path); - document.querySelectorAll('.file-node').forEach(n => n.classList.remove('active')); - el.classList.add('active'); - }); - - el.addEventListener('dragover', e => e.preventDefault()); - el.addEventListener('drop', async (e) => { - e.preventDefault(); - const paths = Array.from(e.dataTransfer.files).map(f => f.path); - for (const p of paths) { - setStatus(`Uploading ${p} to ${owner}/${repo}/${item.path} ...`); - showProgress(0, `Uploading ${ppathBasename(p)} → ${owner}/${repo}/${item.path}`); - const res = await window.electronAPI.uploadAndPush({ localFolder: p, owner, repo, destPath: item.path, cloneUrl: parentNode.previousSibling?.dataset?.giteaClone, branch: 'main' }); - if (!res.ok) setStatus('Upload failed: ' + res.error); - else setStatus(res.usedGit ? `Upload+Push OK (git used)` : `Upload OK (API used)`); - hideProgress(); - } - await expandGiteaDir(el, owner, repo, item.path); - }); - - el.addEventListener('contextmenu', (ev) => { - ev.preventDefault(); - showGiteaContextMenu(el, ev.clientX, ev.clientY); - }); - - container.appendChild(el); - }); - parentNode.after(container); + try { + return p.split(/[\\/]/).pop(); + } catch (_) { + return p; + } } async function previewGiteaFile(owner, repo, filePath) { - setStatus(`Loading file ${filePath}...`); - const res = await window.electronAPI.getGiteaFileContent({ owner, repo, path: filePath, ref: 'main' }); - if (!res.ok) { setStatus('Failed: ' + res.error); $('previewTitle').innerText = filePath; $('previewContent').innerText = ''; return; } - $('previewTitle').innerText = `${owner}/${repo}:${filePath}`; - $('previewContent').innerText = res.content; - setStatus(''); + 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'); + } } -/* Local tree */ -async function refreshLocalTree(folder) { - const res = await window.electronAPI.getFileTree({ folder, exclude: ['node_modules'], maxDepth: 5 }); - const container = $('fileTree'); - const existingLocal = container.querySelector('[data-local-root="1"]'); - if (existingLocal) { - const next = existingLocal.nextSibling; - existingLocal.remove(); - if (next && next.classList && next.classList.contains('local-children')) next.remove(); - } - if (!res.ok) { setStatus('Local tree error: ' + res.error); return; } - const root = document.createElement('div'); - root.className = 'file-node folder'; - root.dataset.localRoot = '1'; - root.innerText = 'Local: ' + folder.split(/[\\/]/).pop(); - root.style.fontWeight = '800'; - root.addEventListener('click', () => { - const next = root.nextSibling; - if (next && next.classList && next.classList.contains('local-children')) next.classList.toggle('hidden'); - }); - const wrapper = document.createElement('div'); - wrapper.className = 'local-children'; - wrapper.style.marginLeft = '8px'; - function build(node, parent) { - const el = document.createElement('div'); - el.className = 'file-node ' + (node.isDirectory ? 'folder' : 'file'); - el.innerText = node.name; - el.dataset.path = node.path; - el.style.paddingLeft = (node.depth * 10 + 8) + 'px'; - el.addEventListener('click', async (ev) => { - ev.stopPropagation(); - if (!node.isDirectory) { - const r = await window.electronAPI.readFile({ path: node.path }); - if (r.ok) { $('previewTitle').innerText = node.path; $('previewContent').innerText = r.content; } - else setStatus('Read failed: ' + r.error); - } +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); }); - parent.appendChild(el); - if (node.children && node.children.length) node.children.forEach(c => build(c, parent)); - } - res.tree.forEach(n => build(n, wrapper)); - const tree = $('fileTree'); - tree.appendChild(root); - root.after(wrapper); } -/* Branches & commits */ -async function loadBranches(folder) { - const res = await window.electronAPI.getBranches({ folder }); - const sel = $('branchSelect'); - sel.innerHTML = ''; - if (res.ok) res.branches.forEach(b => sel.appendChild(new Option(b, b))); - else sel.appendChild(new Option('main','main')); -} -async function loadCommitLogs(folder) { - const res = await window.electronAPI.getCommitLogs({ folder }); - const container = $('logs'); - container.innerHTML = ''; - if (!res.ok) return container.innerText = 'No logs or error: ' + res.error; - res.logs.forEach(l => { const d = document.createElement('div'); d.innerText = l; container.appendChild(d); }); -} +/* ------------------------- + 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(); + } + }); -/* Context menu */ -function showGiteaContextMenu(node, x, y) { - const old = document.getElementById('ctxMenu'); - if (old) old.remove(); - const menu = document.createElement('div'); - menu.id = 'ctxMenu'; - menu.style.position = 'fixed'; - menu.style.left = x + 'px'; - menu.style.top = y + 'px'; - menu.style.background = '#1b1b2a'; - menu.style.border = '1px solid #333'; - menu.style.padding = '6px'; - menu.style.zIndex = '9999'; - menu.style.borderRadius = '6px'; - menu.style.color = '#fff'; - - const owner = node.dataset.giteaOwner; - const repo = node.dataset.giteaRepo; - const filePath = node.dataset.giteaPath || ''; - - if (node.classList.contains('file')) { - const dl = document.createElement('div'); dl.innerText = 'Download File'; dl.style.padding = '6px'; dl.style.cursor = 'pointer'; - dl.onclick = async () => { - const res = await window.electronAPI.downloadGiteaFile({ owner, repo, path: filePath }); - setStatus(res.ok ? `Saved to ${res.savedTo}` : 'Download failed: ' + res.error); - menu.remove(); - }; - menu.appendChild(dl); - } - - if (node.classList.contains('folder')) { - const dlf = document.createElement('div'); dlf.innerText = 'Download Folder (recursive)'; dlf.style.padding = '6px'; dlf.style.cursor = 'pointer'; - dlf.onclick = async () => { - setStatus(`Choose local folder to save ${owner}/${repo}/${filePath} ...`); - showProgress(0, 'Starting download...'); - const res = await window.electronAPI.downloadGiteaFolder({ owner, repo, path: filePath }); - setStatus(res.ok ? `Folder downloaded to ${res.savedTo}` : 'Folder download failed: ' + res.error); - hideProgress(); - menu.remove(); - }; - menu.appendChild(dlf); - } - - const up = document.createElement('div'); up.innerText = 'Upload Local Folder Here'; up.style.padding = '6px'; up.style.cursor = 'pointer'; - up.onclick = async () => { - const folderSel = await window.electronAPI.selectFolder(); - if (!folderSel) { setStatus('No folder selected'); menu.remove(); return; } - setStatus(`Uploading local folder ${folderSel} to ${owner}/${repo}/${filePath} ...`); - showProgress(0, 'Starting upload...'); - const res = await window.electronAPI.uploadAndPush({ localFolder: folderSel, owner, repo, destPath: filePath, cloneUrl: node.dataset.giteaClone, branch: 'main' }); - setStatus(res.ok ? (res.usedGit ? 'Folder uploaded and pushed (git used)' : 'Folder uploaded (API)') : 'Upload failed: ' + res.error); - hideProgress(); - menu.remove(); - if (node.classList.contains('folder')) await expandGiteaDir(node, owner, repo, filePath); - }; - menu.appendChild(up); - - if (node.classList.contains('file')) { - const upf = document.createElement('div'); upf.innerText = 'Upload & Overwrite File'; upf.style.padding = '6px'; upf.style.cursor = 'pointer'; - upf.onclick = async () => { - const sel = await window.electronAPI.selectFile(); - if (!sel.ok || !sel.files || sel.files.length === 0) { setStatus('No file selected'); menu.remove(); return; } - const files = sel.files; - setStatus(`Uploading ${files.length} file(s) to ${owner}/${repo}/${filePath} ...`); - showProgress(0, 'Uploading files...'); - const res = await window.electronAPI.uploadGiteaFile({ localPath: files, owner, repo, destPath: filePath, message: 'Upload via GUI', branch: 'main' }); - setStatus(res.ok ? 'Upload complete' : 'Upload failed: ' + res.error); - hideProgress(); - menu.remove(); - if (node.classList.contains('folder')) await expandGiteaDir(node, owner, repo, filePath); - }; - menu.appendChild(upf); - } - - document.body.appendChild(menu); - document.addEventListener('click', function handler() { menu.remove(); document.removeEventListener('click', handler); }); -} - -/* Context menu for Repository Root */ -function showRepoContextMenu(repoEl, x, y, owner, repoName, cloneUrl) { - const old = document.getElementById('ctxMenu'); - if (old) old.remove(); - - const menu = document.createElement('div'); - menu.id = 'ctxMenu'; - menu.style.position = 'fixed'; - menu.style.left = x + 'px'; - menu.style.top = y + 'px'; - menu.style.background = '#1b1b2a'; - menu.style.border = '1px solid #333'; - menu.style.padding = '6px'; - menu.style.zIndex = '9999'; - menu.style.borderRadius = '6px'; - menu.style.color = '#fff'; - - // Option 1: Download Repository - const dlRepo = document.createElement('div'); - dlRepo.innerText = 'Download Repository'; - dlRepo.style.padding = '6px'; - dlRepo.style.cursor = 'pointer'; - dlRepo.style.borderBottom = '1px solid #333'; - dlRepo.onclick = async () => { - setStatus(`Downloading repository ${owner}/${repoName}...`); - showProgress(0, 'Starting download...'); - const res = await window.electronAPI.downloadGiteaFolder({ owner, repo: repoName, path: '' }); - setStatus(res.ok ? `Repository downloaded to ${res.savedTo}` : 'Download failed: ' + res.error); - hideProgress(); - menu.remove(); - }; - menu.appendChild(dlRepo); - - // Option 2: Upload Folder to Repository - const upRepo = document.createElement('div'); - upRepo.innerText = 'Upload Folder to Repository'; - upRepo.style.padding = '6px'; - upRepo.style.cursor = 'pointer'; - upRepo.style.borderBottom = '1px solid #333'; - upRepo.onclick = async () => { - const folderSel = await window.electronAPI.selectFolder(); - if (!folderSel) { setStatus('No folder selected'); menu.remove(); return; } - setStatus(`Uploading local folder to ${owner}/${repoName} ...`); - showProgress(0, 'Starting upload...'); - const res = await window.electronAPI.uploadAndPush({ localFolder: folderSel, owner, repo: repoName, destPath: '', cloneUrl: cloneUrl, branch: 'main' }); - setStatus(res.ok ? (res.usedGit ? 'Upload & Push successful (git)' : 'Upload successful (API)') : 'Upload failed: ' + res.error); - hideProgress(); - menu.remove(); - if (repoEl._expanded) { - const next = repoEl.nextSibling; - if (next && next.classList && next.classList.contains('gitea-children')) { - next.remove(); - repoEl._expanded = false; - repoEl.click(); // Re-expand - } + // 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); } - }; - menu.appendChild(upRepo); - // Option 3: Delete Repository - const delRepo = document.createElement('div'); - delRepo.innerText = 'Delete Repository'; - delRepo.style.padding = '6px'; - delRepo.style.cursor = 'pointer'; - delRepo.style.color = '#ff5555'; - delRepo.style.fontWeight = 'bold'; - delRepo.onclick = async () => { - if (!confirm(`Are you sure you want to DELETE ${owner}/${repoName}? This cannot be undone.`)) { - menu.remove(); - return; + // Event Handlers + if ($('btnLoadGiteaRepos')) { + $('btnLoadGiteaRepos').onclick = loadGiteaRepos; } - setStatus(`Deleting repository ${owner}/${repoName}...`); - const res = await window.electronAPI.deleteGiteaRepo({ owner, repo: repoName }); - if (res.ok) { - setStatus('Repository deleted successfully'); - repoEl.remove(); - } else { - setStatus('Delete failed: ' + res.error); + + if ($('btnSelectFolder')) { + $('btnSelectFolder').onclick = selectLocalFolder; + } + + if ($('btnPush')) { + $('btnPush').onclick = pushLocalFolder; + } + + if ($('btnCreateRepo')) { + $('btnCreateRepo').onclick = createRepoHandler; } - menu.remove(); - }; - menu.appendChild(delRepo); - document.body.appendChild(menu); - const closeHandler = () => { - menu.remove(); - document.removeEventListener('click', closeHandler); - }; - setTimeout(() => document.addEventListener('click', closeHandler), 10); + 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'); + 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 = ` +
+

📦 Releases für ${repo}

+ +
+ `; + + const newBtn = grid.querySelector('.btn-new-release'); + newBtn.onclick = () => showCreateReleaseModal(owner, repo); + + if (!res.releases || res.releases.length === 0) { + grid.innerHTML += '
📭 Noch keine Releases veröffentlicht
'; + 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'); + } } -/* Start */ -window.addEventListener('DOMContentLoaded', init); \ No newline at end of file +function createReleaseCard(release, isLatest) { + const card = document.createElement('div'); + card.className = 'release-card'; + card.style.cssText = ` + grid-column: 1/-1; + background: var(--bg-tertiary); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: var(--radius-lg); + padding: var(--spacing-xl); + margin-bottom: var(--spacing-lg); + transition: all var(--transition-normal); + `; + + // Header mit Tag und Badges + const header = document.createElement('div'); + header.style.cssText = 'display: flex; gap: 10px; align-items: center; margin-bottom: 12px;'; + + const tag = document.createElement('span'); + tag.textContent = release.tag_name; + tag.style.cssText = ` + background: var(--accent-gradient); + color: #000; + padding: 6px 16px; + border-radius: 20px; + font-weight: 700; + font-size: 14px; + `; + header.appendChild(tag); + + if (isLatest) { + const latestBadge = document.createElement('span'); + latestBadge.textContent = 'LATEST'; + latestBadge.style.cssText = ` + background: var(--success); + color: #000; + padding: 4px 12px; + border-radius: 12px; + font-size: 11px; + font-weight: 700; + `; + header.appendChild(latestBadge); + } + + if (release.prerelease) { + const preBadge = document.createElement('span'); + preBadge.textContent = 'PRE-RELEASE'; + preBadge.style.cssText = ` + background: var(--warning); + color: #000; + padding: 4px 12px; + border-radius: 12px; + font-size: 11px; + font-weight: 700; + `; + header.appendChild(preBadge); + } + + if (release.draft) { + const draftBadge = document.createElement('span'); + draftBadge.textContent = 'DRAFT'; + draftBadge.style.cssText = ` + background: rgba(255, 255, 255, 0.2); + color: var(--text-primary); + padding: 4px 12px; + border-radius: 12px; + font-size: 11px; + font-weight: 700; + `; + header.appendChild(draftBadge); + } + + card.appendChild(header); + + // Title + const title = document.createElement('h3'); + title.textContent = release.name || release.tag_name; + title.style.cssText = 'margin: 0 0 12px 0; color: var(--text-primary); font-size: 20px;'; + card.appendChild(title); + + // Body (Release Notes) + if (release.body) { + const body = document.createElement('div'); + body.className = 'release-body'; + body.innerHTML = parseMarkdownToHTML(release.body); + card.appendChild(body); + } + + // Assets + if (release.assets && release.assets.length > 0) { + const assetsContainer = document.createElement('div'); + assetsContainer.style.cssText = ` + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid rgba(255, 255, 255, 0.05); + `; + + const assetsTitle = document.createElement('div'); + assetsTitle.textContent = '📦 Assets'; + assetsTitle.style.cssText = 'font-weight: 600; margin-bottom: 12px; color: var(--text-primary);'; + assetsContainer.appendChild(assetsTitle); + + release.assets.forEach(asset => { + const assetItem = document.createElement('div'); + assetItem.style.cssText = ` + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + background: rgba(255, 255, 255, 0.03); + border-radius: var(--radius-sm); + margin-bottom: 8px; + `; + + const assetName = document.createElement('span'); + assetName.textContent = `📎 ${asset.name}`; + assetName.style.cssText = 'color: var(--text-primary);'; + + const assetSize = document.createElement('span'); + assetSize.textContent = formatBytes(asset.size || 0); + assetSize.style.cssText = 'color: var(--text-muted); font-size: 12px; margin-left: 12px;'; + + const downloadBtn = document.createElement('button'); + downloadBtn.textContent = '⬇️ Download'; + downloadBtn.style.cssText = ` + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 6px 12px; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 12px; + `; + downloadBtn.onclick = () => { + if (asset.browser_download_url) { + window.open(asset.browser_download_url, '_blank'); + } + }; + + const deleteAssetBtn = document.createElement('button'); + deleteAssetBtn.textContent = '🗑️'; + deleteAssetBtn.style.cssText = ` + background: transparent; + color: var(--danger); + border: 1px solid var(--danger); + padding: 6px 10px; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 12px; + margin-left: 8px; + `; + deleteAssetBtn.onclick = async () => { + if (confirm(`Delete asset "${asset.name}"?`)) { + const res = await window.electronAPI.deleteReleaseAsset({ + owner: currentReleaseView.owner, + repo: currentReleaseView.repo, + assetId: asset.id + }); + if (res.ok) { + assetItem.remove(); + setStatus('Asset deleted'); + } + } + }; + + const leftSide = document.createElement('div'); + leftSide.style.cssText = 'display: flex; align-items: center; gap: 12px;'; + leftSide.appendChild(assetName); + leftSide.appendChild(assetSize); + + const rightSide = document.createElement('div'); + rightSide.style.cssText = 'display: flex; gap: 8px;'; + rightSide.appendChild(downloadBtn); + rightSide.appendChild(deleteAssetBtn); + + assetItem.appendChild(leftSide); + assetItem.appendChild(rightSide); + assetsContainer.appendChild(assetItem); + }); + + card.appendChild(assetsContainer); + } + + // Meta Info + const meta = document.createElement('div'); + meta.style.cssText = ` + display: flex; + gap: 20px; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid rgba(255, 255, 255, 0.05); + color: var(--text-muted); + font-size: 12px; + `; + + const date = new Date(release.created_at); + const dateStr = date.toLocaleDateString('de-DE', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + + meta.innerHTML = ` + 📅 ${dateStr} + 👤 ${release.author?.login || 'Unknown'} + `; + card.appendChild(meta); + + // Action Buttons + const actions = document.createElement('div'); + actions.style.cssText = 'display: flex; gap: 12px; margin-top: 16px;'; + + const downloadArchiveBtn = document.createElement('button'); + downloadArchiveBtn.textContent = '📦 Download ZIP'; + downloadArchiveBtn.style.cssText = ` + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 8px 16px; + border-radius: var(--radius-md); + cursor: pointer; + font-weight: 600; + `; + downloadArchiveBtn.onclick = async () => { + const res = await window.electronAPI.downloadReleaseArchive({ + owner: currentReleaseView.owner, + repo: currentReleaseView.repo, + tag: release.tag_name + }); + if (res.ok) { + setStatus(`Downloaded to ${res.savedTo}`); + } + }; + + const addAssetBtn = document.createElement('button'); + addAssetBtn.textContent = '📎 Add Asset'; + addAssetBtn.style.cssText = ` + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 8px 16px; + border-radius: var(--radius-md); + cursor: pointer; + font-weight: 600; + `; + addAssetBtn.onclick = () => showUploadAssetDialog(release); + + const deleteBtn = document.createElement('button'); + deleteBtn.textContent = '🗑️ Delete'; + deleteBtn.style.cssText = ` + background: transparent; + color: var(--danger); + border: 1px solid var(--danger); + padding: 8px 16px; + border-radius: var(--radius-md); + cursor: pointer; + font-weight: 600; + margin-left: auto; + `; + deleteBtn.onclick = async () => { + if (confirm(`Delete release "${release.name || release.tag_name}"?`)) { + const res = await window.electronAPI.deleteRelease({ + owner: currentReleaseView.owner, + repo: currentReleaseView.repo, + releaseId: release.id + }); + if (res.ok) { + card.remove(); + setStatus('Release deleted'); + } + } + }; + + actions.appendChild(downloadArchiveBtn); + actions.appendChild(addAssetBtn); + actions.appendChild(deleteBtn); + card.appendChild(actions); + + return card; +} + +/* ------------------------- + CREATE RELEASE MODAL + ------------------------- */ +function showCreateReleaseModal(owner, repo) { + const modal = document.createElement('div'); + modal.className = 'modal'; + modal.innerHTML = ` +
+

🚀 Neues Release erstellen

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ `; + + 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 = ` +

+ 📊 Commit History + + ${currentCommitView.repo} / ${currentCommitView.branch} + +

+ +
+ + +
+ `; + + grid.appendChild(header); + + // Search-Handler + const searchInput = header.querySelector('#commitSearch'); + const clearBtn = header.querySelector('#btnClearSearch'); + + let searchTimeout; + searchInput.addEventListener('input', (e) => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + handleCommitSearch(e.target.value); + }, 300); + }); + + clearBtn.onclick = () => { + searchInput.value = ''; + renderCommitTimeline(currentCommitView.commits); + }; + + // Timeline Container + const timelineContainer = document.createElement('div'); + timelineContainer.id = 'commitTimeline'; + timelineContainer.style.cssText = ` + position: relative; + max-width: 100%; + `; + + grid.appendChild(timelineContainer); + + // Initial render + renderCommitTimeline(currentCommitView.commits); +} + +function renderCommitTimeline(commits) { + const container = $('commitTimeline'); + if (!container) return; + + container.innerHTML = ''; + + if (!commits || commits.length === 0) { + container.innerHTML = '
📭 No commits found
'; + return; + } + + // Timeline mit Cards + commits.forEach((commit, index) => { + const card = createCommitCard(commit, index); + container.appendChild(card); + }); +} + +function createCommitCard(commit, index) { + const card = document.createElement('div'); + card.className = 'commit-card'; + card.dataset.sha = commit.sha; + + const isEven = index % 2 === 0; + + card.style.cssText = ` + position: relative; + padding-left: 60px; + margin-bottom: 32px; + cursor: pointer; + transition: all var(--transition-normal); + `; + + // Timeline dot + const dot = document.createElement('div'); + dot.style.cssText = ` + position: absolute; + left: 18px; + top: 0; + width: 16px; + height: 16px; + background: var(--accent-primary); + border: 3px solid var(--bg-primary); + border-radius: 50%; + z-index: 2; + box-shadow: 0 0 0 4px var(--bg-tertiary); + `; + card.appendChild(dot); + + // Timeline line + if (index < currentCommitView.commits.length - 1) { + const line = document.createElement('div'); + line.style.cssText = ` + position: absolute; + left: 25px; + top: 16px; + width: 2px; + height: calc(100% + 32px); + background: linear-gradient(180deg, var(--accent-primary) 0%, rgba(0, 212, 255, 0.2) 100%); + z-index: 1; + `; + card.appendChild(line); + } + + // Content card + const content = document.createElement('div'); + content.className = 'commit-content'; + content.style.cssText = ` + background: var(--bg-tertiary); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + transition: all var(--transition-normal); + `; + + // Commit message + const message = commit.commit?.message || commit.message || 'No message'; + const shortMessage = message.split('\n')[0]; // First line only + + const messageEl = document.createElement('div'); + messageEl.style.cssText = ` + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 12px; + line-height: 1.4; + `; + messageEl.textContent = shortMessage; + content.appendChild(messageEl); + + // Meta info + const meta = document.createElement('div'); + meta.style.cssText = ` + display: flex; + gap: 20px; + flex-wrap: wrap; + font-size: 13px; + color: var(--text-muted); + margin-bottom: 12px; + `; + + const author = commit.commit?.author?.name || commit.author?.login || 'Unknown'; + const date = new Date(commit.commit?.author?.date || commit.created_at); + const dateStr = formatRelativeTime(date); + const sha = commit.sha?.substring(0, 7) || '???????'; + + meta.innerHTML = ` + + 👤 ${author} + + + 🕐 ${dateStr} + + + #${sha} + + `; + content.appendChild(meta); + + // Stats (if available) + if (commit.stats) { + const stats = document.createElement('div'); + stats.style.cssText = ` + display: flex; + gap: 16px; + font-size: 12px; + padding-top: 12px; + border-top: 1px solid rgba(255, 255, 255, 0.05); + `; + + stats.innerHTML = ` + +${commit.stats.additions || 0} + -${commit.stats.deletions || 0} + ${commit.stats.total || 0} changes + `; + content.appendChild(stats); + } + + card.appendChild(content); + + // Hover effect + card.addEventListener('mouseenter', () => { + content.style.borderColor = 'var(--accent-primary)'; + content.style.transform = 'translateX(4px)'; + content.style.boxShadow = 'var(--shadow-md)'; + }); + + card.addEventListener('mouseleave', () => { + content.style.borderColor = 'rgba(255, 255, 255, 0.1)'; + content.style.transform = 'translateX(0)'; + content.style.boxShadow = 'none'; + }); + + // Click to show details + card.onclick = () => showCommitDetails(commit); + + return card; +} + +/* ------------------------- + COMMIT DETAILS & DIFF VIEWER + ------------------------- */ +async function showCommitDetails(commit) { + currentCommitView.selectedCommit = commit; + + const modal = document.createElement('div'); + modal.className = 'modal commit-modal'; + modal.innerHTML = ` +
+
+

📋 Commit Details

+ +
+ +
+
+
+ Loading commit details... +
+
+
+
+ `; + + document.body.appendChild(modal); + + $('btnCloseCommitModal').onclick = () => modal.remove(); + modal.onclick = (e) => { + if (e.target === modal) modal.remove(); + }; + + // Load commit details + await loadCommitDetailsContent(commit); +} + +async function loadCommitDetailsContent(commit) { + const container = $('commitDetailsContent'); + if (!container) return; + + try { + // Check if this is local git or Gitea repo + let diffRes, filesRes; + + if (selectedFolder) { + // Local Git repository + const details = await window.electronAPI.getLocalCommitDetails({ + folderPath: selectedFolder, + sha: commit.sha || commit.hash + }); + + diffRes = { ok: true, diff: details?.diff || '' }; + filesRes = { ok: true, files: details?.fileChanges?.files || [], stats: { additions: details?.fileChanges?.insertions || 0, deletions: details?.fileChanges?.deletions || 0 } }; + } else { + // Gitea repository + const [diff, files] = await Promise.all([ + window.electronAPI.getCommitDiff({ + owner: currentCommitView.owner, + repo: currentCommitView.repo, + sha: commit.sha + }), + window.electronAPI.getCommitFiles({ + owner: currentCommitView.owner, + repo: currentCommitView.repo, + sha: commit.sha + }) + ]); + + diffRes = diff; + filesRes = files; + } + + container.innerHTML = ''; + + // Commit info header + const header = document.createElement('div'); + header.style.cssText = ` + background: var(--bg-tertiary); + padding: var(--spacing-xl); + border-radius: var(--radius-lg); + margin-bottom: 24px; + `; + + const message = commit.commit?.message || commit.message || 'No message'; + const author = commit.commit?.author?.name || commit.author?.login || 'Unknown'; + const email = commit.commit?.author?.email || ''; + const date = new Date(commit.commit?.author?.date || commit.created_at); + const sha = commit.sha || ''; + + header.innerHTML = ` +

${escapeHtml(message)}

+
+ 👤 ${escapeHtml(author)} ${email ? `<${escapeHtml(email)}>` : ''} + 🕐 ${date.toLocaleString()} + ${sha.substring(0, 7)} +
+ `; + container.appendChild(header); + + // File changes summary + if (filesRes.ok && filesRes.files && Array.isArray(filesRes.files) && filesRes.files.length > 0) { + const filesHeader = document.createElement('div'); + filesHeader.style.cssText = ` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + `; + + filesHeader.innerHTML = ` +

+ 📁 Changed Files (${filesRes.files.length}) +

+
+ +${filesRes.stats?.additions || 0} + -${filesRes.stats?.deletions || 0} +
+ `; + container.appendChild(filesHeader); + + // File list + const fileList = document.createElement('div'); + fileList.style.cssText = 'margin-bottom: 24px;'; + + filesRes.files.forEach(file => { + const fileItem = document.createElement('div'); + fileItem.style.cssText = ` + display: flex; + justify-content: space-between; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.03); + border-radius: var(--radius-sm); + margin-bottom: 4px; + font-size: 13px; + `; + + const changeType = file.changes === file.insertions ? 'added' : + file.changes === file.deletions ? 'deleted' : 'modified'; + + const icon = changeType === 'added' ? '🆕' : + changeType === 'deleted' ? '🗑️' : '📝'; + + fileItem.innerHTML = ` + ${icon} ${escapeHtml(file.file)} + + +${file.insertions} + -${file.deletions} + + `; + fileList.appendChild(fileItem); + }); + + container.appendChild(fileList); + } + + // Diff viewer + if (diffRes.ok && diffRes.diff) { + const diffHeader = document.createElement('h4'); + diffHeader.textContent = '📝 Changes (Diff)'; + diffHeader.style.marginBottom = '12px'; + container.appendChild(diffHeader); + + const diffContainer = document.createElement('div'); + diffContainer.className = 'diff-viewer'; + diffContainer.style.cssText = ` + background: #1e1e1e; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: var(--radius-md); + padding: 16px; + overflow-x: auto; + font-family: 'Courier New', monospace; + font-size: 13px; + line-height: 1.6; + max-height: 600px; + overflow-y: auto; + `; + + diffContainer.innerHTML = formatDiff(diffRes.diff); + container.appendChild(diffContainer); + } + + } catch (error) { + console.error('Error loading commit details:', error); + console.error('Stack:', error.stack); + console.error('selectedFolder:', selectedFolder); + console.error('commit:', commit); + + const errorMsg = error.message || String(error); + const isLocalGit = selectedFolder ? 'Local Git' : 'Gitea'; + + container.innerHTML = `
+

❌ Error loading commit details

+

+ Source: ${isLocalGit}
+ Error: ${escapeHtml(errorMsg)} +

+
`; + } +} + +function formatDiff(diffText) { + const lines = diffText.split('\n'); + let html = ''; + + lines.forEach(line => { + let color = '#d4d4d4'; + let bgColor = 'transparent'; + + if (line.startsWith('+++') || line.startsWith('---')) { + color = '#569cd6'; // Blue + } else if (line.startsWith('+')) { + color = '#4ec9b0'; // Green + bgColor = 'rgba(78, 201, 176, 0.1)'; + } else if (line.startsWith('-')) { + color = '#f48771'; // Red + bgColor = 'rgba(244, 135, 113, 0.1)'; + } else if (line.startsWith('@@')) { + color = '#c586c0'; // Purple + } else if (line.startsWith('diff')) { + color = '#dcdcaa'; // Yellow + } + + html += `
${escapeHtml(line)}
`; + }); + + return html; +} + +/* ------------------------- + COMMIT SEARCH + ------------------------- */ +async function handleCommitSearch(query) { + if (!query || query.trim().length === 0) { + renderCommitTimeline(currentCommitView.commits); + return; + } + + setStatus('Searching commits...'); + + try { + const res = await window.electronAPI.searchCommits({ + owner: currentCommitView.owner, + repo: currentCommitView.repo, + branch: currentCommitView.branch, + query: query.trim() + }); + + if (res.ok) { + renderCommitTimeline(res.commits); + setStatus(`Found ${res.commits.length} commits`); + } else { + setStatus('Search failed'); + } + } catch (error) { + console.error('Search error:', error); + setStatus('Search error'); + } +} + +/* ------------------------- + HELPER FUNCTIONS + ------------------------- */ +function formatRelativeTime(date) { + const now = new Date(); + const diffMs = now - date; + const diffMins = Math.floor(diffMs / 60000); + + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'just now'; + if (diffMins < 60) return `${diffMins} min ago`; + if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; + if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; + if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) > 1 ? 's' : ''} ago`; + if (diffDays < 365) return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) > 1 ? 's' : ''} ago`; + return `${Math.floor(diffDays / 365)} year${Math.floor(diffDays / 365) > 1 ? 's' : ''} ago`; +} + +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +/* ------------------------- + EVENT LISTENERS + ------------------------- */ + +// File Editor Event Listeners +setTimeout(() => { + // Buttons + const btnClose = $('btnCloseEditor'); + const btnSave = $('btnEditorSave'); + const btnSearch = $('btnEditorSearch'); + const btnDiscard = $('btnDiscardEdit'); + const btnFileActions = $('btnFileActions'); + const modal = $('fileEditorModal'); + + // Close button + if (btnClose) btnClose.addEventListener('click', closeFileEditor); + + // Save button + if (btnSave) btnSave.addEventListener('click', saveCurrentFile); + + // Search button + if (btnSearch) btnSearch.addEventListener('click', toggleSearch); + + // Discard button + if (btnDiscard) btnDiscard.addEventListener('click', closeFileEditor); + + // File actions menu + if (btnFileActions) { + btnFileActions.addEventListener('click', (e) => { + const menu = $('fileActionsMenu'); + if (menu) { + menu.classList.toggle('hidden'); + const rect = btnFileActions.getBoundingClientRect(); + menu.style.top = (rect.bottom + 4) + 'px'; + menu.style.right = '20px'; + } + }); + } + + // Search & Replace + const searchInput = $('searchInput'); + if (searchInput) { + searchInput.addEventListener('keyup', performSearch); + searchInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + replaceOnce(); + } + }); + } + + const btnReplace = $('btnReplace'); + if (btnReplace) btnReplace.addEventListener('click', replaceOnce); + + const btnReplaceAll = $('btnReplaceAll'); + if (btnReplaceAll) btnReplaceAll.addEventListener('click', replaceAll); + + const btnCloseSearch = $('btnCloseSearch'); + if (btnCloseSearch) btnCloseSearch.addEventListener('click', () => { + const searchBar = $('searchBar'); + if (searchBar) searchBar.classList.add('hidden'); + }); + + // Textarea events + const textarea = $('fileEditorContent'); + if (textarea) { + textarea.addEventListener('input', () => { + updateEditorContent(textarea.value); + }); + + textarea.addEventListener('scroll', () => { + const lineNumbers = $('lineNumbers'); + if (lineNumbers) lineNumbers.scrollTop = textarea.scrollTop; + }); + + textarea.addEventListener('click', updateEditorStats); + textarea.addEventListener('keyup', updateEditorStats); + } + + // Close modal on background click + if (modal) { + modal.addEventListener('click', (e) => { + if (e.target === modal) { + closeFileEditor(); + } + }); + } + + console.log('✅ Advanced editor event listeners registered'); +}, 100); + +// Global keyboard shortcuts +document.addEventListener('keydown', (e) => { + const modal = $('fileEditorModal'); + if (!modal || modal.classList.contains('hidden')) return; + + // Ctrl+S / Cmd+S - Save + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault(); + saveCurrentFile(); + } + + // Ctrl+F / Cmd+F - Search + if ((e.ctrlKey || e.metaKey) && e.key === 'f') { + e.preventDefault(); + toggleSearch(); + } + + // Ctrl+H / Cmd+H - Replace + if ((e.ctrlKey || e.metaKey) && e.key === 'h') { + e.preventDefault(); + toggleSearch(); + $('replaceInput').focus(); + } + + // ESC - Close search bar if open + if (e.key === 'Escape') { + const searchBar = $('searchBar'); + if (searchBar && !searchBar.classList.contains('hidden')) { + searchBar.classList.add('hidden'); + } + } +}); + diff --git a/renderer/style.css b/renderer/style.css index 544648c..db0eb27 100644 --- a/renderer/style.css +++ b/renderer/style.css @@ -1,53 +1,1386 @@ -/* Dark Explorer Style */ :root { - --bg: #141423; - --panel: #1f1f2f; - --muted: #9aa0b4; - --accent: #00bcd4; - --card: #242438; - --panel-2: #2b2b3d; + /* Moderne Farbpalette */ + --bg-primary: #0f0f1e; + --bg-secondary: #1a1a2e; + --bg-tertiary: #242438; + --surface: #2d2d4a; + --surface-hover: #363656; + + /* Akzentfarben */ + --accent-primary: #00d4ff; + --accent-secondary: #8b5cf6; + --accent-gradient: linear-gradient(135deg, #00d4ff 0%, #8b5cf6 100%); + + /* Textfarben */ + --text-primary: #f0f0f5; + --text-secondary: #a8a8c0; + --text-muted: #6b6b80; + + /* Status Farben */ + --success: #22c55e; + --warning: #f59e0b; + --danger: #ef4444; + --info: #3b82f6; + + /* Schatten und Effekte */ + --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5); + --shadow-glow: 0 0 20px rgba(0, 212, 255, 0.3); + + /* Übergänge */ + --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1); + + /* Abstände */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 12px; + --spacing-lg: 16px; + --spacing-xl: 24px; + --spacing-2xl: 32px; + + /* Border Radius */ + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; + --radius-xl: 18px; } -html,body { height:100%; margin:0; background:var(--bg); color:#e6e6e6; font-family: "Segoe UI", Roboto, Arial; } -#toolbar { display:flex; align-items:center; gap:8px; padding:10px; background:var(--panel); border-bottom:1px solid #2f2f4f; } -#toolbar button, #toolbar select { background:var(--accent); color:#022; border:none; padding:6px 10px; border-radius:6px; cursor:pointer; } -#toolbar .status { margin-left:8px; color:var(--muted); font-weight:600; } +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} -#main { display:flex; height: calc(100vh - 52px); gap:10px; padding:12px; box-sizing:border-box; } -aside#explorerPanel { width:320px; background:var(--panel-2); border-radius:8px; padding:10px; overflow:auto; box-shadow: 0 6px 18px rgba(0,0,0,0.6); } -#fileTree { font-family: monospace; font-size:13px; } +body { + margin: 0; + background: var(--bg-primary); + color: var(--text-primary); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + overflow: hidden; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} -#detailsPanel { flex:1; display:flex; flex-direction:column; gap:12px; overflow:auto; } -.card { background:var(--card); padding:12px; border-radius:8px; box-shadow: 0 6px 18px rgba(0,0,0,0.6); } -input, select { background:#222231; color:#e6e6e6; border:1px solid #3a3a5a; padding:6px; border-radius:6px; width:100%; box-sizing:border-box; } +/* =========================== + TOOLBAR + =========================== */ +#toolbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-sm) var(--spacing-lg); + background: linear-gradient(135deg, rgba(26, 26, 46, 0.9) 0%, rgba(36, 36, 56, 0.8) 100%); + border-bottom: 2px solid; + border-image: linear-gradient(90deg, var(--accent-primary) 0%, var(--accent-secondary) 100%) 1; + height: 56px; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + position: relative; + z-index: 100; + box-shadow: 0 8px 32px rgba(0, 212, 255, 0.08); + transition: all var(--transition-normal); +} + +#toolbar::before { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(ellipse 100% 100% at 50% 0%, rgba(0, 212, 255, 0.05) 0%, transparent 70%); + pointer-events: none; + border-radius: inherit; +} + +#toolbar::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: var(--accent-gradient); + opacity: 0.3; +} + +.tool-group { + display: flex; + gap: var(--spacing-md); + align-items: center; + position: relative; + z-index: 1; +} + +.tool-group:not(:last-child)::after { + content: ''; + width: 1px; + height: 32px; + background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.1), transparent); + margin-left: var(--spacing-md); +} + +/* =========================== + BUTTONS - VEREINHEITLICHT + =========================== */ +#toolbar button, +#toolbar select { + background: linear-gradient(135deg, rgba(45, 45, 74, 0.8) 0%, rgba(54, 54, 86, 0.6) 100%); + color: var(--text-primary); + border: 1px solid rgba(255, 255, 255, 0.15); + height: 36px; + min-width: 100px; + padding: 0 var(--spacing-md); + border-radius: var(--radius-md); + cursor: pointer; + font-weight: 600; + font-size: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-sm); + transition: all var(--transition-normal); + position: relative; + overflow: hidden; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08); +} + +#toolbar button::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.3), transparent); + transition: left var(--transition-slow); + z-index: 1; +} + +#toolbar button::after { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.1) 0%, transparent 70%); + pointer-events: none; +} + +#toolbar button:hover { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.6) 0%, rgba(139, 92, 246, 0.4) 100%); + border-color: var(--accent-primary); + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 212, 255, 0.25), + inset 0 1px 0 rgba(255, 255, 255, 0.15), + 0 0 20px rgba(0, 212, 255, 0.15); +} + +#toolbar button:hover::before { + left: 100%; +} + +#toolbar button:active { + transform: translateY(0); + box-shadow: 0 4px 12px rgba(0, 212, 255, 0.15), inset 0 2px 4px rgba(0, 0, 0, 0.3); +} + +#toolbar button:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.2), + 0 8px 24px rgba(0, 212, 255, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +#toolbar button.hidden { + display: none; +} + +#toolbar select { + background: linear-gradient(135deg, rgba(26, 26, 46, 0.9) 0%, rgba(36, 36, 56, 0.8) 100%); + border: 1px solid rgba(255, 255, 255, 0.15); + cursor: pointer; + padding: 0 var(--spacing-md); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} + +#toolbar select:hover { + border-color: var(--accent-primary); + background: linear-gradient(135deg, rgba(45, 45, 74, 0.9) 0%, rgba(54, 54, 86, 0.8) 100%); + box-shadow: 0 8px 24px rgba(0, 212, 255, 0.15); +} + +#toolbar select:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.15), + 0 8px 24px rgba(0, 212, 255, 0.15); +} + +/* =========================== + STATUS + =========================== */ +.status { + color: var(--text-secondary); + font-size: 12px; + font-weight: 600; + margin-left: var(--spacing-lg); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 250px; + padding: var(--spacing-sm) var(--spacing-lg); + background: linear-gradient(135deg, rgba(45, 45, 74, 0.6) 0%, rgba(36, 36, 56, 0.4) 100%); + border-radius: var(--radius-md); + border: 1px solid rgba(0, 212, 255, 0.2); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + box-shadow: 0 4px 12px rgba(0, 212, 255, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.05); + transition: all var(--transition-normal); +} + +.status:hover { + border-color: rgba(0, 212, 255, 0.4); + box-shadow: 0 6px 16px rgba(0, 212, 255, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.08); +} + +/* =========================== + MAIN CONTENT + =========================== */ +#main { + height: calc(100vh - 56px); + padding: var(--spacing-xl); + overflow-y: auto; + overflow-x: hidden; + transition: background var(--transition-normal), border var(--transition-normal); + border-radius: var(--radius-md); + margin: var(--spacing-sm); + position: relative; +} + +/* Global Drop Zone Indicator */ +#main.drop-active { + background: rgba(0, 212, 255, 0.05); + border: 2px dashed var(--accent-primary); +} + +#main.drop-active::before { + content: '📁 Dateien hier ablegen zum Hochladen'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 24px; + font-weight: 700; + color: var(--accent-primary); + background: var(--bg-secondary); + padding: var(--spacing-2xl) var(--spacing-2xl); + border-radius: var(--radius-lg); + border: 2px solid var(--accent-primary); + box-shadow: var(--shadow-glow); + pointer-events: none; + z-index: 999; + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + } + 50% { + transform: translate(-50%, -50%) scale(1.05); + opacity: 0.8; + } +} + +/* Custom Scrollbar */ +#main::-webkit-scrollbar { + width: 10px; +} + +#main::-webkit-scrollbar-track { + background: var(--bg-primary); +} + +#main::-webkit-scrollbar-thumb { + background: var(--surface); + border-radius: 5px; + border: 2px solid var(--bg-primary); +} + +#main::-webkit-scrollbar-thumb:hover { + background: var(--surface-hover); +} + +/* =========================== + EXPLORER GRID + =========================== */ +.explorer-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: var(--spacing-lg); + animation: fadeIn var(--transition-slow) ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* =========================== + ITEM CARDS + =========================== */ +.item-card { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: var(--spacing-xl); + border-radius: var(--radius-lg); + cursor: pointer; + transition: all var(--transition-normal); + border: 1px solid rgba(255, 255, 255, 0.05); + background: var(--bg-tertiary); + position: relative; + overflow: hidden; +} + +.item-card::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(0, 212, 255, 0.1) 0%, transparent 70%); + opacity: 0; + transition: opacity var(--transition-normal); +} + +.item-card:hover::before { + opacity: 1; +} + +.item-card:hover { + background: var(--surface); + border-color: var(--accent-primary); + transform: translateY(-4px) scale(1.02); + box-shadow: var(--shadow-lg), var(--shadow-glow); +} + +.item-card:active { + transform: translateY(-2px) scale(1.01); +} + +.item-card.drag-target { + border-color: var(--success); + background: rgba(34, 197, 94, 0.1); + transform: scale(1.05); + box-shadow: 0 0 20px rgba(34, 197, 94, 0.3); +} + +.item-icon { + font-size: 56px; + margin-bottom: var(--spacing-md); + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)); + transition: transform var(--transition-normal); +} + +.item-card:hover .item-icon { + transform: scale(1.1) rotate(-5deg); +} + +.item-name { + font-size: 13px; + font-weight: 600; + word-break: break-word; + color: var(--text-primary); + line-height: 1.4; + max-width: 100%; + transition: color var(--transition-fast); +} + +.item-card:hover .item-name { + color: var(--accent-primary); +} + +/* =========================== + MODALS + =========================== */ +.modal { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn var(--transition-normal) ease-out; + pointer-events: auto; +} + +.modal.hidden { + display: none !important; + pointer-events: none; +} + +.hidden { + display: none !important; +} + +.card { + background: var(--bg-secondary); + padding: var(--spacing-2xl); + border-radius: var(--radius-xl); + width: 480px; + max-width: 90vw; + box-shadow: var(--shadow-lg); + border: 1px solid rgba(255, 255, 255, 0.08); + animation: slideUp var(--transition-normal) ease-out; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.card h2 { + margin-bottom: var(--spacing-xl); + color: var(--text-primary); + font-size: 24px; + font-weight: 700; + background: var(--accent-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* =========================== + FORM ELEMENTS + =========================== */ +.input-group { + margin-bottom: var(--spacing-lg); +} + +.input-group label { + display: block; + margin-bottom: var(--spacing-sm); + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +input, +select { + width: 100%; + padding: var(--spacing-md) var(--spacing-lg); + 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; + transition: all var(--transition-normal); +} + +input:focus, +select:focus { + outline: none; + border-color: var(--accent-primary); + background: var(--surface); + box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1); +} + +input::placeholder { + color: var(--text-muted); +} -/* FIX: Styling für Checkboxen, damit sie nicht die volle Breite einnehmen */ input[type="checkbox"] { width: auto; - margin-right: 8px; - vertical-align: middle; + margin-right: var(--spacing-sm); cursor: pointer; + accent-color: var(--accent-primary); } -.file-node { padding:6px 8px; border-radius:6px; margin:2px 0; } -.file-node:hover { background: rgba(255,255,255,0.02); cursor:pointer; } -.file-node.folder { font-weight:700; color:#cfefff; } -.file-node.file { color:#dfe7ff; } +/* =========================== + MODAL BUTTONS - VEREINHEITLICHT + =========================== */ +.modal-buttons { + display: flex; + gap: var(--spacing-md); + margin-top: var(--spacing-xl); +} -.preview-content { background:#0f1020; color:#dcecff; padding:10px; border-radius:6px; height:260px; overflow:auto; white-space:pre-wrap; font-family: Consolas, Monaco, monospace; } +.modal-buttons button { + flex: 1; + height: 42px; + border-radius: var(--radius-md); + font-weight: 600; + font-size: 14px; + cursor: pointer; + transition: all var(--transition-normal); + border: 1px solid rgba(255, 255, 255, 0.1); + background: var(--bg-tertiary); + color: var(--text-primary); +} -#logs { max-height:220px; overflow:auto; font-family:monospace; font-size:13px; background:#0b0b12; padding:8px; border-radius:6px; } -#logs div { padding:6px; border-bottom:1px solid #1b1b2a; } +.modal-buttons button:hover { + background: var(--surface); + border-color: var(--accent-primary); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} -.modal { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; background:rgba(0,0,0,0.6); } -.modalContent { width:420px; padding:16px; } -.modal.hidden { display:none; } +.modal-buttons button:active { + transform: translateY(0); +} +.modal-buttons button.accent-btn { + background: linear-gradient(135deg, rgba(0, 212, 255, 0.3) 0%, rgba(139, 92, 246, 0.2) 100%); + border-color: var(--accent-primary); + color: var(--accent-primary); + font-weight: 700; +} + +.modal-buttons button.accent-btn:hover { + background: linear-gradient(135deg, rgba(0, 212, 255, 0.5) 0%, rgba(139, 92, 246, 0.4) 100%); + box-shadow: 0 0 20px rgba(0, 212, 255, 0.3); +} + +.modal-buttons button.secondary { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.2); +} + +.modal-buttons button.secondary:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.3); +} + +/* =========================== + CONTEXT MENU + =========================== */ .context-menu { - font-family: sans-serif; - font-size: 13px; - box-shadow: 0 2px 10px rgba(0,0,0,0.5); + position: fixed; + background: var(--bg-secondary); + border: 1px solid rgba(255, 255, 255, 0.1); + padding: var(--spacing-sm); + border-radius: var(--radius-md); + z-index: 10000; + min-width: 200px; + box-shadow: var(--shadow-lg); + animation: fadeIn var(--transition-fast) ease-out; } -.context-menu div:hover { - background-color: #555; + +.context-item { + padding: var(--spacing-md) var(--spacing-lg); + cursor: pointer; + color: var(--text-primary); + font-size: 13px; + font-weight: 500; + border-radius: var(--radius-sm); + transition: all var(--transition-fast); + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.context-item:hover { + background: var(--accent-gradient); + color: #000; + transform: translateX(4px); +} + +/* =========================== + PROGRESS BAR (falls verwendet) + =========================== */ +progress { + width: 100%; + height: 8px; + border: none; + border-radius: var(--radius-sm); + overflow: hidden; + background: var(--bg-tertiary); +} + +progress::-webkit-progress-bar { + background: var(--bg-tertiary); + border-radius: var(--radius-sm); +} + +progress::-webkit-progress-value { + background: var(--accent-gradient); + border-radius: var(--radius-sm); + transition: width var(--transition-normal); +} + +progress::-moz-progress-bar { + background: var(--accent-gradient); + border-radius: var(--radius-sm); +} + +/* =========================== + RESPONSIVE DESIGN + =========================== */ +@media (max-width: 768px) { + #toolbar { + flex-wrap: wrap; + height: auto; + padding: var(--spacing-md); + } + + .tool-group { + width: 100%; + justify-content: space-between; + } + + .explorer-grid { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: var(--spacing-md); + } + + .card { + width: 100%; + padding: var(--spacing-xl); + } + + .status { + width: 100%; + max-width: 100%; + margin: var(--spacing-sm) 0 0 0; + } +} + +/* =========================== + UTILITY CLASSES + =========================== */ +.text-center { + text-align: center; +} + +.mt-1 { margin-top: var(--spacing-xs); } +.mt-2 { margin-top: var(--spacing-sm); } +.mt-3 { margin-top: var(--spacing-md); } +.mt-4 { margin-top: var(--spacing-lg); } + +.mb-1 { margin-bottom: var(--spacing-xs); } +.mb-2 { margin-bottom: var(--spacing-sm); } +.mb-3 { margin-bottom: var(--spacing-md); } +.mb-4 { margin-bottom: var(--spacing-lg); } + +.p-1 { padding: var(--spacing-xs); } +.p-2 { padding: var(--spacing-sm); } +.p-3 { padding: var(--spacing-md); } +.p-4 { padding: var(--spacing-lg); } + +/* =========================== + RELEASE CARDS + =========================== */ +.release-card { + background: var(--bg-tertiary); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: var(--radius-lg); + padding: var(--spacing-xl); + transition: all var(--transition-normal); +} + +.release-card:hover { + border-color: rgba(255, 255, 255, 0.2); + box-shadow: var(--shadow-md); +} + +.release-header { + display: flex; + gap: var(--spacing-md); + align-items: center; + margin-bottom: var(--spacing-md); + flex-wrap: wrap; +} + +.release-tag { + background: var(--accent-gradient); + color: #000; + padding: 6px 16px; + border-radius: 20px; + font-weight: 700; + font-size: 14px; + display: inline-block; +} + +.release-badge { + padding: 4px 12px; + border-radius: 12px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.release-badge.latest { + background: var(--success); + color: #000; +} + +.release-badge.prerelease { + background: var(--warning); + color: #000; +} + +.release-badge.draft { + background: rgba(255, 255, 255, 0.2); + color: var(--text-primary); +} + +.release-title { + margin: 0 0 var(--spacing-md) 0; + color: var(--text-primary); + font-size: 20px; + font-weight: 700; +} + +.release-body { + color: var(--text-secondary); + line-height: 1.8; + margin-bottom: var(--spacing-lg); + white-space: normal; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.release-body br { + display: block; + content: ''; + margin: 0; +} + +.release-body br + br { + margin-bottom: var(--spacing-md); +} + +.release-body > p { + margin: var(--spacing-md) 0; + color: inherit; +} + +.release-body > div { + margin: var(--spacing-md) 0; + text-align: left; +} + +.release-body > div[align="center"] { + text-align: center; + margin: var(--spacing-lg) 0; + padding: var(--spacing-lg) 0; +} + +.release-body > div[align="center"] h1, +.release-body > div[align="center"] h2, +.release-body > div[align="center"] h3, +.release-body > div[align="center"] h4 { + color: var(--text-primary); + margin: var(--spacing-md) 0; + font-weight: 700; +} + +.release-body > div[align="center"] p { + margin: var(--spacing-sm) 0; + font-style: italic; +} + +.release-body h1, +.release-body h2, +.release-body h3, +.release-body h4, +.release-body h5, +.release-body h6 { + color: var(--text-primary); + margin: var(--spacing-lg) 0 var(--spacing-md) 0; + font-weight: 700; + line-height: 1.4; +} + +.release-body h1 { font-size: 28px; } +.release-body h2 { font-size: 24px; } +.release-body h3 { font-size: 20px; } +.release-body h4 { font-size: 18px; } +.release-body h5 { font-size: 16px; } +.release-body h6 { font-size: 14px; } + +.release-body p { + margin: var(--spacing-md) 0; + color: inherit; +} + +.release-body em, +.release-body i { + font-style: italic; + color: var(--accent-primary); +} + +.release-body strong, +.release-body b { + color: var(--text-primary); + font-weight: 700; +} + +.release-body ul, +.release-body ol { + margin: var(--spacing-md) 0 var(--spacing-md) var(--spacing-xl); +} + +.release-body li { + margin: var(--spacing-sm) 0; +} + +.release-body hr { + border: none; + border-top: 1px solid rgba(255, 255, 255, 0.1); + margin: var(--spacing-lg) 0; +} + +.release-body code { + background: rgba(255, 255, 255, 0.05); + color: var(--accent-primary); + padding: 2px 6px; + border-radius: 4px; + font-family: 'Courier New', monospace; + font-size: 13px; +} + +.release-body pre { + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + padding: var(--spacing-md); + border-radius: var(--radius-md); + overflow-x: auto; + margin: var(--spacing-md) 0; +} + +.release-body blockquote { + border-left: 3px solid var(--accent-primary); + padding-left: var(--spacing-md); + margin: var(--spacing-md) 0; + color: var(--text-muted); + font-style: italic; +} + +.release-assets { + margin-top: var(--spacing-lg); + padding-top: var(--spacing-lg); + border-top: 1px solid rgba(255, 255, 255, 0.05); +} + +.release-assets-title { + font-weight: 600; + margin-bottom: var(--spacing-md); + color: var(--text-primary); + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.release-asset-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-md); + background: rgba(255, 255, 255, 0.03); + border-radius: var(--radius-sm); + margin-bottom: var(--spacing-sm); + transition: background var(--transition-fast); +} + +.release-asset-item:hover { + background: rgba(255, 255, 255, 0.05); +} + +.release-asset-info { + display: flex; + align-items: center; + gap: var(--spacing-md); +} + +.release-asset-name { + color: var(--text-primary); + font-weight: 500; +} + +.release-asset-size { + color: var(--text-muted); + font-size: 12px; +} + +.release-asset-actions { + display: flex; + gap: var(--spacing-sm); +} + +.release-meta { + display: flex; + gap: var(--spacing-xl); + margin-top: var(--spacing-lg); + padding-top: var(--spacing-lg); + border-top: 1px solid rgba(255, 255, 255, 0.05); + color: var(--text-muted); + font-size: 12px; +} + +.release-actions { + display: flex; + gap: var(--spacing-md); + margin-top: var(--spacing-lg); + flex-wrap: wrap; +} + +.btn-danger { + background: transparent !important; + color: var(--danger) !important; + border: 1px solid var(--danger) !important; +} + +.btn-danger:hover { + background: var(--danger) !important; + color: #000 !important; +} + +/* Release Modal spezifisches */ +.release-modal textarea { + font-family: 'Courier New', monospace; + line-height: 1.5; +} + +/* =========================== + COMMIT HISTORY & TIMELINE + =========================== */ +.commit-card { + position: relative; + animation: slideInLeft 0.3s ease-out; +} + +@keyframes slideInLeft { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.commit-content { + position: relative; +} + +.commit-modal .card { + animation: slideUp 0.3s ease-out; +} + +.commit-modal .diff-viewer::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.commit-modal .diff-viewer::-webkit-scrollbar-track { + background: #1e1e1e; +} + +.commit-modal .diff-viewer::-webkit-scrollbar-thumb { + background: #3a3a3a; + border-radius: 4px; +} + +.commit-modal .diff-viewer::-webkit-scrollbar-thumb:hover { + background: #4a4a4a; +} + +/* Diff Syntax Highlighting */ +.diff-viewer { + font-feature-settings: "liga" 0; + text-rendering: optimizeLegibility; +} + +.diff-line-added { + background: rgba(78, 201, 176, 0.1); + color: #4ec9b0; +} + +.diff-line-removed { + background: rgba(244, 135, 113, 0.1); + color: #f48771; +} + +.diff-line-meta { + color: #c586c0; + font-weight: 600; +} + +/* Commit Search Box */ +#commitSearch { + transition: all var(--transition-normal); +} + +#commitSearch:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1); +} + +/* Timeline Animation */ +@keyframes pulse-dot { + 0%, 100% { + box-shadow: 0 0 0 0 rgba(0, 212, 255, 0.7); + } + 50% { + box-shadow: 0 0 0 8px rgba(0, 212, 255, 0); + } +} + +.commit-card:first-child .commit-dot { + animation: pulse-dot 2s infinite; +} + +/* =========================== + FILE EDITOR + =========================== */ +.file-editor-card { + background: var(--bg-secondary); + border-radius: var(--radius-xl); + width: 60vw; + max-width: 900px; + height: 85vh; + max-height: 800px; + display: flex; + flex-direction: column; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + border: 1px solid rgba(0, 212, 255, 0.2); + animation: slideUp var(--transition-normal) ease-out; + overflow: hidden; + position: relative; + pointer-events: auto; +} + +.file-editor-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-lg); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + background: linear-gradient(135deg, rgba(45, 45, 74, 0.8) 0%, rgba(36, 36, 56, 0.6) 100%); + gap: var(--spacing-md); + flex-shrink: 0; +} + +.file-editor-title { + display: flex; + align-items: center; + gap: var(--spacing-md); + font-size: 18px; + font-weight: 600; + color: var(--text-primary); +} + +.file-editor-toolbar { + display: flex; + gap: var(--spacing-sm); +} + +.editor-tool-btn { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.1); + color: var(--text-secondary); + cursor: pointer; + padding: 6px 12px; + border-radius: var(--radius-md); + font-size: 14px; + transition: all var(--transition-fast); + display: flex; + align-items: center; + gap: 4px; +} + +.editor-tool-btn:hover { + background: rgba(0, 212, 255, 0.1); + border-color: var(--accent-primary); + color: var(--accent-primary); +} + +/* Tab Bar */ +.file-editor-tabs { + display: flex; + gap: 2px; + padding: 8px 16px 0; + background: var(--bg-primary); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + overflow-x: auto; + flex-shrink: 0; + max-height: 50px; + align-items: flex-end; +} + +.file-editor-tabs::-webkit-scrollbar { + height: 4px; +} + +.file-editor-tabs::-webkit-scrollbar-thumb { + background: rgba(0, 212, 255, 0.3); + border-radius: 2px; +} + +.editor-tab { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: rgba(45, 45, 74, 0.5); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: var(--radius-md) var(--radius-md) 0 0; + color: var(--text-secondary); + font-size: 12px; + cursor: pointer; + transition: all var(--transition-fast); + white-space: nowrap; + min-width: 100px; + justify-content: space-between; +} + +.editor-tab:hover { + background: rgba(45, 45, 74, 0.8); + border-color: rgba(0, 212, 255, 0.3); +} + +.editor-tab.active { + background: linear-gradient(135deg, rgba(0, 212, 255, 0.2) 0%, rgba(122, 81, 255, 0.15) 100%); + border-color: var(--accent-primary); + color: var(--accent-primary); + border-bottom-color: var(--bg-primary); +} + +.editor-tab-name { + flex: 1; + display: flex; + align-items: center; + gap: 6px; +} + +.editor-tab-dirty { + font-weight: bold; + color: #ff6b6b; +} + +.editor-tab-close { + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 0; + font-size: 14px; + opacity: 0.6; + transition: all var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; +} + +.editor-tab-close:hover { + opacity: 1; + color: #ff6b6b; +} + +/* Search Bar */ +.search-bar { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-md) var(--spacing-lg); + background: rgba(45, 45, 74, 0.6); + border-bottom: 1px solid rgba(0, 212, 255, 0.2); + flex-shrink: 0; + animation: slideDown var(--transition-fast) ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.search-input { + padding: 6px 12px; + background: rgba(36, 36, 56, 0.8); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 12px; + min-width: 150px; +} + +.search-input:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1); +} + +.search-input::placeholder { + color: var(--text-muted); +} + +.search-btn { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.1); + color: var(--text-secondary); + cursor: pointer; + padding: 6px 10px; + border-radius: var(--radius-sm); + font-size: 12px; + transition: all var(--transition-fast); +} + +.search-btn:hover { + background: rgba(0, 212, 255, 0.1); + border-color: var(--accent-primary); + color: var(--accent-primary); +} + +.search-info { + font-size: 11px; + color: var(--text-muted); + min-width: 60px; + text-align: right; +} + +/* Editor Container mit Line Numbers */ +.file-editor-container { + display: flex; + flex: 1; + overflow: hidden; + position: relative; +} + +.line-numbers { + background: rgba(36, 36, 56, 0.8); + border-right: 1px solid rgba(255, 255, 255, 0.1); + padding: var(--spacing-lg) 8px; + font-family: 'Courier New', 'Monaco', monospace; + font-size: 13px; + line-height: 1.6; + color: var(--text-muted); + text-align: right; + white-space: pre; + overflow: hidden; + flex-shrink: 0; + user-select: none; + width: 50px; +} + +.file-editor-textarea { + flex: 1; + padding: var(--spacing-lg); + background: var(--bg-primary); + color: var(--text-primary); + border: none; + outline: none; + font-family: 'Courier New', 'Monaco', monospace; + font-size: 13px; + line-height: 1.6; + resize: none; + overflow: auto; + white-space: pre; + word-wrap: normal; + tab-size: 4; + -moz-tab-size: 4; + margin: 0; +} + + +.file-editor-textarea::placeholder { + color: var(--text-muted); +} + +.file-editor-textarea::-webkit-scrollbar { + width: 10px; +} + +.file-editor-textarea::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +.file-editor-textarea::-webkit-scrollbar-thumb { + background: var(--surface); + border-radius: 5px; +} + +.file-editor-textarea::-webkit-scrollbar-thumb:hover { + background: var(--surface-hover); +} + +.file-editor-footer { + padding: var(--spacing-lg); + border-top: 1px solid rgba(255, 255, 255, 0.1); + background: linear-gradient(135deg, rgba(36, 36, 56, 0.8) 0%, rgba(26, 26, 46, 0.6) 100%); + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--spacing-lg); + flex-shrink: 0; +} + +.file-editor-info { + display: flex; + gap: var(--spacing-xl); + flex: 1; + flex-wrap: wrap; +} + +.file-path, +.file-stats, +.file-cursor { + font-size: 11px; + color: var(--text-secondary); +} + +.file-cursor { + color: var(--accent-primary); + font-weight: 600; +} + +.auto-save-status { + font-size: 11px; + color: #00d455; + font-weight: 600; + animation: fadeInOut 2s ease-in-out; +} + +@keyframes fadeInOut { + 0%, 100% { + opacity: 0; + } + 50% { + opacity: 1; + } } \ No newline at end of file