window.addEventListener('DOMContentLoaded', async () => { // Elemente holen const folderList = document.getElementById('folderList'); const addBtn = document.getElementById('addFolderBtn'); const titleEl = document.getElementById('currentTitle'); const treeviewEl = document.getElementById('folderHierarchyDropdown'); const titleArrow = document.getElementById('folderTitleArrow'); const contentList = document.getElementById('contentList'); const readmeBtn = document.getElementById('readmeBtn'); const initRepoBtn = document.getElementById('initRepoBtn'); const pushBtn = document.getElementById('pushBtn'); const panel = document.querySelector('.flex-1.p-4.overflow-y-auto'); const PAGE_SIZE = 50; const paginationEl = document.createElement('div'); paginationEl.className = 'pagination flex justify-center items-center my-2 space-x-2'; contentList.parentElement.insertBefore(paginationEl, contentList); let lastFolderPath = null; let lastPage = null; const slot = document.getElementById('catSlot'); window.cat = new window.AnimeCat(slot, { images: { default: 'assets/cat/default.png', eyesClosed: 'assets/cat/eyes_closed.png', blink: 'assets/cat/blink.png', mouthOpen: 'assets/cat/mouth_open.png', joy: 'assets/cat/joy.png', mischievous: 'assets/cat/mischievous.png' } }); // Readme-Button nur einmal binden readmeBtn.addEventListener('click', async () => { const selected = await window.electronAPI.getSelected(); if (!selected) return alert('No folder selected!'); readmeBtn.disabled = true; readmeBtn.textContent = 'Generating...'; try { const output = await window.electronAPI.generateReadme(selected.path); alert('README.md was generated!\n\n' + output.slice(0, 500) + '…'); } catch (e) { alert('Error generating README:\n' + (e.message || e)); } readmeBtn.disabled = false; const hasReadme = await window.electronAPI.hasReadme(selected.path); readmeBtn.textContent = hasReadme ? 'Update README' : 'Generate README'; }); pushBtn.addEventListener('click', async () => { const selected = await window.electronAPI.getSelected(); if (!selected || !selected.path) { return alert('No folder selected to push!'); } pushBtn.disabled = true; pushBtn.textContent = 'Pushing…'; try { // send the folder‐path to main via a new IPC channel 'push-to-gitea' const result = await window.electronAPI.pushToGitea(selected.path); if (result.success) { alert('✔ Pushed successfully to Gitea: ' + result.repoUrl); } else { alert('❌ Push failed:\n' + result.error); } } catch (err) { alert('❌ Unexpected error:\n' + (err.message || err)); } finally { pushBtn.disabled = false; pushBtn.textContent = 'Push to Gitea'; } }); initRepoBtn.addEventListener('click', async () => { const selected = await window.electronAPI.getSelected(); if (!selected || !selected.path) { return alert('No folder selected!'); } initRepoBtn.disabled = true; initRepoBtn.textContent = 'Initializing…'; try { const result = await window.electronAPI.initRepo(selected.path); if (!result?.success) { throw new Error(result?.error || 'Init failed'); } await renderSidebar(); const refreshed = await window.electronAPI.getSelected(); if (refreshed) { await renderContent(refreshed); } } catch (err) { alert('Init failed:\n' + (err.message || err)); } finally { initRepoBtn.disabled = false; initRepoBtn.textContent = 'Init Repo'; } }); // Drag and Drop document.body.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; }); document.body.addEventListener('drop', async e => { e.preventDefault(); const files = [...e.dataTransfer.files]; if (!files.length) return; for (let f of files) { if (f.type === "") { await window.electronAPI.addFolderByPath(f.path); await renderSidebar(); const sel = await window.electronAPI.getSelected(); if (sel) await renderContent(sel); } } }); // Sky-Mode Setup const DAY_COLOR = [173, 216, 230]; const NIGHT_COLOR = [0, 0, 50]; function lerpColor(c1, c2, t) { return c1.map((v, i) => Math.round(v + t * (c2[i] - v))); } function getTimeFactor() { const now = new Date(); const md = now.getHours() * 60 + now.getMinutes(); if (md < 4 * 60) return 0; if (md < 8 * 60) return (md - 4*60) / (4*60); if (md < 16 * 60) return 1; if (md < 20 * 60) return 1 - ((md - 16*60) / (4*60)); return 0; } function updateBackground() { const factor = getTimeFactor(); const [r,g,b] = lerpColor(NIGHT_COLOR, DAY_COLOR, factor); panel.style.backgroundColor = `rgb(${r}, ${g}, ${b})`; } let skyIntervalId, titleIntervalId; function applySkyMode(enabled) { document.body.classList.toggle('sky-mode', enabled); clearInterval(skyIntervalId); clearInterval(titleIntervalId); if (enabled) { updateBackground(); skyIntervalId = setInterval(updateBackground, 60_000); function updateAllTextColors() { setTextColor('sky'); } updateAllTextColors(); titleIntervalId = setInterval(updateAllTextColors, 60_000); } else { panel.style.backgroundColor = ''; setTextColor('default'); } } const initialSky = await window.settingsAPI.getSkyMode(); applySkyMode(initialSky); window.addEventListener('skymode-changed', e => applySkyMode(e.detail)); function setTextColor(mode) { let textColor = '#111'; if (mode === 'sky') { const hour = new Date().getHours(); textColor = (hour >= 18 || hour < 6) ? '#fff' : '#111'; } titleEl.style.color = textColor; treeviewEl.style.color = textColor; titleArrow.style.color = textColor; paginationEl.style.color = textColor; if (treeviewEl.querySelectorAll) { treeviewEl.querySelectorAll('.tree-file, .tree-dir').forEach(el => el.style.color = textColor); } } function basename(fullPath) { return fullPath.replace(/.*[\\/]/, ''); } async function getFolderObjByPath(path) { const folders = await window.electronAPI.getFolders(); return folders.find(f => f.path === path) || null; } async function renderSidebar() { const folders = await window.electronAPI.getFolders(); const selected = await window.electronAPI.getSelected(); folderList.innerHTML = ''; for (const folderObj of folders) { const folder = folderObj.path; const isMonitoring = folderObj.monitoring; const li = document.createElement('li'); li.setAttribute('data-folder-id', encodeURIComponent(folder)); li.className = [ 'flex items-center justify-between px-3 py-2 rounded cursor-pointer', selected && folder === selected.path ? 'selected' : '', folderObj.needsRelocation ? 'needs-relocation' : '' ].join(' '); li.innerHTML = `
${basename(folder)}
${ folderObj.needsRelocation ? `!` : '' }
`; // Folder-Klick (inkl. Relocation-Logik) li.addEventListener('click', async e => { if (e.target.closest('.pause-play-btn, .remove-btn')) return; if (folderObj.needsRelocation) { const paths = await window.electronAPI.pickFolder(); const newPath = paths && paths[0]; if (!newPath) return; const isGit = await window.electronAPI.isGitRepo(newPath); if (!isGit) { alert('Das ist kein gültiges Git-Repository.'); return; } const lastKnownHash = folderObj.lastHeadHash; if (!lastKnownHash) { alert('Kein gespeicherter Hash – Vergleich nicht möglich.'); return; } const isMatch = await window.electronAPI.repoHasCommit(newPath, lastKnownHash); if (!isMatch) { alert('Das ist nicht das ursprüngliche Repo (Commit-Hash fehlt).'); return; } await window.electronAPI.relocateFolder(folderObj.path, newPath); await renderSidebar(); const newFolderObj = (await window.electronAPI.getFolders()) .find(f => f.path === newPath); if (newFolderObj) { await window.electronAPI.setSelected(newFolderObj); await renderContent(newFolderObj); } return; } await window.electronAPI.setSelected(folderObj); await renderSidebar(); await renderContent(folderObj); }); // Monitoring-Button const pauseBtn = li.querySelector('.pause-play-btn'); pauseBtn.addEventListener('click', async e => { e.stopPropagation(); await window.electronAPI.setMonitoring(folderObj, !isMonitoring); await renderSidebar(); }); // Remove-Button const removeBtn = li.querySelector('.remove-btn'); removeBtn.addEventListener('click', async e => { e.stopPropagation(); const count = await window.electronAPI.getCommitCount(folderObj); const hasUnstaged= await window.electronAPI.hasDiffs(folderObj); const skipPrompt = await window.settingsAPI.getSkipPrompt(); if (count === 1 && !hasUnstaged) { if (skipPrompt) { await window.electronAPI.removeGitFolder(folderObj); } else { const ok = confirm( 'Dieser Ordner hat nur einen Initial-Commit und keine Änderungen.\n' + 'Möchtest du das gesamte Git-Repository (den .git-Ordner) löschen?' ); if (ok) { await window.electronAPI.removeGitFolder(folderObj); } else { return; } } } await window.electronAPI.removeFolder(folderObj); await renderSidebar(); const all = await window.electronAPI.getFolders(); if (all.length === 0) { titleEl.textContent = 'No folder selected'; contentList.innerHTML = ''; } else { const idxOld = folders.findIndex(f => f.path === folderObj.path); let idxNew = Math.max(0, idxOld - 1); const pick = all[idxNew]; await window.electronAPI.setSelected(pick); await renderSidebar(); await renderContent(pick); } }); // Kontextmenü li.addEventListener('contextmenu', e => { e.preventDefault(); window.electronAPI.showFolderContextMenu(folderObj.path); }); folderList.appendChild(li); } } let countdownInterval = null; function getCommitColor(commitCount) { const stops = [ { c: 0, color: [157, 157, 157] }, { c: 5, color: [255, 255, 255] }, { c: 15, color: [30, 255, 0] }, { c: 50, color: [0, 112, 221] }, { c: 100, color: [163, 53, 238] }, { c: 500, color: [255, 128, 0] } ]; function lerp(a, b, t) { return a + (b - a) * t; } function lerpColor(a, b, t) { return [ Math.round(lerp(a[0], b[0], t)), Math.round(lerp(a[1], b[1], t)), Math.round(lerp(a[2], b[2], t)) ]; } let lower = stops[0], upper = stops[stops.length-1]; for (let i = 0; i < stops.length - 1; ++i) { if (commitCount >= stops[i].c && commitCount < stops[i+1].c) { lower = stops[i]; upper = stops[i+1]; break; } } const range = upper.c - lower.c || 1; const t = Math.min(Math.max((commitCount - lower.c) / range, 0), 1); return lerpColor(lower.color, upper.color, t); } function formatCountdown(ms) { if (!ms || ms <= 0) return '00:00'; const s = Math.floor(ms / 1000); const m = Math.floor(s / 60).toString().padStart(2, '0'); const sec = (s % 60).toString().padStart(2, '0'); return `${m}:${sec}`; } async function updateInteractionBar(folderObj) { const stats = await window.electronAPI.getDailyCommitStats(); const today = new Date().toISOString().slice(0, 10); const commitsToday = stats[today] || 0; const intelligentCommitThreshold = await window.electronAPI.getIntelligentCommitThreshold?.() || 20; const minutesCommitThreshold = await window.electronAPI.getMinutesCommitThreshold?.() || 5; const linesChanged = folderObj?.linesChanged || 0; const linesUntilRewrite = Math.max(0, intelligentCommitThreshold - linesChanged); let countdown = "00:00"; let msLeft = 0; if (folderObj?.firstCandidateBirthday) { const msThreshold = minutesCommitThreshold * 60 * 1000; const endTime = new Date(folderObj.firstCandidateBirthday).getTime() + msThreshold; msLeft = Math.max(0, endTime - Date.now()); countdown = formatCountdown(msLeft); } const [r, g, b] = getCommitColor(commitsToday); document.getElementById('commitsToday').textContent = commitsToday; document.getElementById('commitsToday').style.color = `rgb(${r},${g},${b})`; document.getElementById('linesUntilRewrite').textContent = linesUntilRewrite; document.getElementById('countdown').textContent = countdown; startLiveCountdown(folderObj, msLeft); } async function startLiveCountdown(folderObj, msLeft) { if (countdownInterval) clearInterval(countdownInterval); if (!folderObj?.firstCandidateBirthday || msLeft <= 0) { document.getElementById('countdown').textContent = "00:00"; return; } const minutesCommitThreshold = await window.electronAPI.getMinutesCommitThreshold?.() || 5; const msThreshold = minutesCommitThreshold * 60 * 1000; const endTime = new Date(folderObj.firstCandidateBirthday).getTime() + msThreshold; countdownInterval = setInterval(() => { const msL = Math.max(0, endTime - Date.now()); document.getElementById('countdown').textContent = formatCountdown(msL); if (msL <= 0) clearInterval(countdownInterval); }, 1000); } const folderTitleDrop = document.getElementById('folderTitleDrop'); const folderTitleArrow = document.getElementById('folderTitleArrow'); const folderHierarchyDropdown = document.getElementById('folderHierarchyDropdown'); let isDropdownOpen = false; folderTitleDrop.addEventListener('click', async () => { if (isDropdownOpen) { closeDropdown(); return; } const selected = await window.electronAPI.getSelected(); if (!selected || !selected.path) return; folderHierarchyDropdown.textContent = 'Lade Verzeichnis…'; folderHierarchyDropdown.classList.remove('hidden'); folderTitleArrow.classList.add('open'); isDropdownOpen = true; const tree = await window.electronAPI.getFolderTree(selected.path); folderHierarchyDropdown.innerHTML = renderFolderTreeAscii(tree, '.', ''); setTextColor(document.body.classList.contains('sky-mode') ? 'sky' : 'default'); }); folderHierarchyDropdown.addEventListener('contextmenu', function(e) { const el = e.target.closest('.tree-file, .tree-dir'); if (!el) return; e.preventDefault(); const relPath = el.getAttribute('data-path'); const type = el.getAttribute('data-type'); const selected = window.electronAPI.getSelectedSync?.() || (window.currentSelectedFolderObj || {}); const absPath = selected.path + '/' + relPath; window.electronAPI.showTreeContextMenu({ absPath, relPath, root: selected.path, type }); }); function closeDropdown() { folderHierarchyDropdown.classList.add('hidden'); folderTitleArrow.classList.remove('open'); isDropdownOpen = false; } function renderFolderTreeAscii(tree, prefix = '', indent = '', relPath = '.') { if (!Array.isArray(tree)) return ''; let result = ''; const lastIdx = tree.length - 1; tree.forEach((node, i) => { const isLast = i === lastIdx; const pointer = isLast ? '└── ' : '├── '; const thisRelPath = relPath === '.' ? node.name : relPath + '/' + node.name; if (node.type === 'dir') { result += `${indent}${pointer}${node.name}/\n`; const newIndent = indent + (isLast ? ' ' : '│ '); result += renderFolderTreeAscii(node.children, '', newIndent, thisRelPath); } else { result += `${indent}${pointer}${node.name}\n`; } }); return result; } async function getCommitPageForHash(folderObj, hash, pageSize = PAGE_SIZE) { const { commits } = await window.electronAPI.getCommits(folderObj, 1, 100000); const idx = commits.findIndex(c => c.hash === hash || hash.startsWith(c.hash)); if (idx === -1) return 1; return Math.floor(idx / pageSize) + 1; } async function renderContent(folderObj, page) { closeDropdown(); const folder = folderObj.path; await updateInteractionBar(folderObj); titleEl.textContent = folder; setTextColor(document.body.classList.contains('sky-mode') ? 'sky' : 'default'); const isGit = await window.electronAPI.isGitRepo(folder); initRepoBtn.classList.toggle('hidden', isGit); initRepoBtn.disabled = isGit; readmeBtn.disabled = !isGit; pushBtn.disabled = !isGit; pushBtn.classList.toggle('hidden', !isGit); if (!isGit) { contentList.innerHTML = '
Not a Git repository. Click "Init Repo" to initialize.
'; paginationEl.innerHTML = ''; lastFolderPath = folder; lastPage = null; return; } else { initRepoBtn.classList.add('hidden'); } const commitCount = await window.electronAPI.getCommitCount(folderObj); const hasCommits = commitCount > 0; pushBtn.classList.toggle('hidden', !hasCommits); pushBtn.disabled = !hasCommits; // Jetzt erst den Readme-Button-Text aktualisieren: const hasReadme = await window.electronAPI.hasReadme(folder); readmeBtn.textContent = hasReadme ? 'Update README' : 'Generate README'; let usePage = page; if (!usePage || folder !== lastFolderPath) { const { head } = await window.electronAPI.getCommits(folderObj, 1, 1); usePage = await getCommitPageForHash(folderObj, head, PAGE_SIZE); } lastFolderPath = folder; lastPage = usePage; const { head, commits, total, page: currentPage, pageSize, pages } = await window.electronAPI.getCommits(folderObj, usePage, PAGE_SIZE); if (!commits || !commits.length) { contentList.innerHTML = '
No commits found.
'; paginationEl.innerHTML = ''; return; } contentList.innerHTML = commits.map(c => { const isQueued = folderObj.llmCandidates && folderObj.llmCandidates.some(fullHash => fullHash.startsWith(c.hash) ); return `
  • ${c.hash} ${new Date(c.date).toLocaleString()}
    ${c.message}
    
              
    ${ isQueued ? `In Rewrite Queue` : '' }
  • `; }).join(''); // Pagination if (pages > 1) { paginationEl.innerHTML = ` Seite ${currentPage} / ${pages} `; paginationEl.querySelector('#page-prev').onclick = () => renderContent(folderObj, currentPage - 1); paginationEl.querySelector('#page-next').onclick = () => renderContent(folderObj, currentPage + 1); paginationEl.style.display = 'flex'; } else { paginationEl.innerHTML = ''; paginationEl.style.display = 'none'; } // Diff-Buttons überprüfen contentList.querySelectorAll('.diff-btn').forEach(async btn => { const hash = btn.dataset.hash; const diffText = await window.electronAPI.diffCommit(folderObj, hash); if (!diffText.trim()) { btn.disabled = true; btn.classList.add('disabled'); } }); // Diff-Toggle & Hervorhebung contentList.querySelectorAll('.diff-btn').forEach(btn => { btn.addEventListener('click', async () => { const li = btn.closest('li'); const hash = btn.dataset.hash; const svg = btn.querySelector('svg'); const container = li.querySelector('.diff-container'); const pre = container.querySelector('pre'); if (!pre.innerHTML.trim()) { const diff = await window.electronAPI.diffCommit(folderObj, hash); const escaped = diff .replace(/&/g, '&') .replace(//g, '>'); pre.innerHTML = escaped .split('\n') .map(line => { const cls = line.startsWith('+') ? 'diff-line addition' : line.startsWith('-') ? 'diff-line deletion' : 'diff-line'; return `
    ${line}
    `; }) .join(''); } const isOpen = container.classList.toggle('open'); if (isOpen) { container.style.maxHeight = container.scrollHeight + 'px'; } else { container.style.maxHeight = '0'; } svg.classList.toggle('open', isOpen); }); }); // Snapshot-Button contentList.querySelectorAll('.snapshot-btn').forEach(btn => { btn.addEventListener('click', async () => { const hash = btn.dataset.hash; try { const savedPath = await window.electronAPI.snapshotCommit(folderObj, hash); if (savedPath) { alert(`Snapshot gespeichert unter:\n${savedPath}`); } } catch (err) { console.error(err); alert('Snapshot fehlgeschlagen'); } }); }); // Checkout-Button contentList.querySelectorAll('.checkout-btn').forEach(btn => { btn.addEventListener('click', async () => { const hash = btn.dataset.hash; await window.electronAPI.checkoutCommit(folderObj, hash); const page = await getCommitPageForHash(folderObj, hash, PAGE_SIZE); await renderContent(folderObj, page); }); }); // Copy-Diff-Button contentList.querySelectorAll('.diff-container').forEach(container => { const btn = container.querySelector('.copy-diff-btn'); const pre = container.querySelector('pre'); const originalSVG = btn.innerHTML; const checkSVG = ` `; btn.addEventListener('click', () => { navigator.clipboard.writeText(pre.textContent) .then(() => { btn.innerHTML = checkSVG; setTimeout(() => { btn.innerHTML = originalSVG; }, 1000); }) .catch(err => { console.error('Clipboard write failed', err); }); }); }); const currentEl = contentList.querySelector('li.current-commit'); if (currentEl) { currentEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } await renderSidebar(); const initial = await window.electronAPI.getSelected(); if (initial) await renderContent(initial); addBtn.addEventListener('click', async () => { await window.electronAPI.addFolder(); await renderSidebar(); const sel = await window.electronAPI.getSelected(); if (sel) await renderContent(sel); }); window.addEventListener('repo-updated', async e => { closeDropdown(); const obj = await getFolderObjByPath(e.detail); if (!obj) return; const selected = await window.electronAPI.getSelected(); if (!selected || selected.path !== obj.path) { await window.electronAPI.setSelected(obj); await renderSidebar(); } await renderContent(obj); }); titleEl.addEventListener('contextmenu', e => { e.preventDefault(); if (titleEl.textContent !== 'No folder selected') { window.electronAPI.showFolderContextMenu(titleEl.textContent); } }); window.electronAPI.onTrayToggleMonitoring(async (_e, folderPath) => { const folders = await window.electronAPI.getFolders(); const folder = folders.find(f => f.path === folderPath); if (folder) { await window.electronAPI.setMonitoring(folder, !folder.monitoring); await renderSidebar(); const selected = await window.electronAPI.getSelected(); if (selected && selected.path === folderPath) { await renderContent(folder); } } }); window.electronAPI.onTrayRemoveFolder(async (_e, folderPath) => { const folders = await window.electronAPI.getFolders(); const folder = folders.find(f => f.path === folderPath); if (folder) { await window.electronAPI.removeFolder(folder); await renderSidebar(); const all = await window.electronAPI.getFolders(); if (all.length === 0) { titleEl.textContent = 'No folder selected'; contentList.innerHTML = ''; } else { await window.electronAPI.setSelected(all[0]); await renderContent(all[0]); } } }); window.electronAPI.onTrayAddFolder(async () => { await window.electronAPI.addFolder(); await renderSidebar(); const sel = await window.electronAPI.getSelected(); if (sel) await renderContent(sel); }); window.electronAPI.onFoldersLocationUpdated(folderObj => { const selector = `[data-folder-id="${encodeURIComponent(folderObj.path)}"]`; const li = document.querySelector(selector); if (li) { if (folderObj.needsRelocation) { li.classList.add('needs-relocation'); li.setAttribute('disabled', ''); } else { li.classList.remove('needs-relocation'); } renderSidebar(); } }); window.electronAPI.onCatBegin(() => window.cat && window.cat.beginSpeech()); window.electronAPI.onCatChunk((_e, chunk) => window.cat && window.cat.appendSpeech(chunk)); window.electronAPI.onCatEnd(() => window.cat && window.cat.endSpeech()); const speakToCat = msg => { window.cat.beginSpeech(); let i = 0; function nextChar() { if (i < msg.length) { window.cat.appendSpeech(msg[i++]); setTimeout(nextChar, 50); } else { window.cat.endSpeech(); } } nextChar(); }; window.electronAPI.getDailyCommitStats().then(stats => { const today = new Date().toISOString().slice(0, 10); const todayCount = stats[today] || 0; window.updateCatGlow(todayCount); }); window.updateCatGlow = function(commitCount) { if (window.cat) window.cat.animateCatGlow(commitCount); }; });