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 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';
}
});
// 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 = `
${
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');
// 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
? `
`
: ''
}
`;
}).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);
};
});