diff --git a/main.js b/main.js index 157dd71..c8fef7b 100644 --- a/main.js +++ b/main.js @@ -47,6 +47,7 @@ function createWindow() { return win; } + // Settings-Fenster let settingsWin; function openSettings(win) { @@ -101,6 +102,87 @@ async function initGitRepo(folder) { } } + +// Map für Monitoring-Watcher (nicht repoWatchers!) +const monitoringWatchers = new Map(); + +function startMonitoringWatcher(folderPath, win) { + // Nicht mehrfach starten + if (monitoringWatchers.has(folderPath)) return; + const watcher = chokidar.watch(folderPath, { + ignored: /(^|[\/\\])\..|node_modules|\.git/, // ignoriert .git und .dot-Dateien + node_modules + ignoreInitial: true, + persistent: true, + depth: 99, // Rekursiv + awaitWriteFinish: { + stabilityThreshold: 300, + pollInterval: 100 + } + }); + + // TODO: Optionale .gitignore Logik nachrüsten + + watcher.on('all', async (event, changedPath) => { + debug(`[MONITOR] ${event} in ${changedPath}`); + // Hier einfach auto-commit Funktion rufen: + await autoCommit(folderPath, `[auto] ${event} ${path.relative(folderPath, changedPath)}`); + // Repo-UI aktualisieren: + win.webContents.send('repo-updated', folderPath); + }); + + monitoringWatchers.set(folderPath, watcher); + debug(`[MONITOR] Watcher aktiv für ${folderPath}`); +} + +function stopMonitoringWatcher(folderPath) { + const watcher = monitoringWatchers.get(folderPath); + if (watcher) { + watcher.close(); + monitoringWatchers.delete(folderPath); + debug(`[MONITOR] Watcher gestoppt für ${folderPath}`); + } +} + +async function autoCommit(folder, message) { + const git = simpleGit(folder); + const status = await git.status(); + if ( + status.not_added.length === 0 && + status.created.length === 0 && + status.deleted.length === 0 && + status.modified.length === 0 && + status.renamed.length === 0 + ) { + debug('Auto-Commit: Keine Änderungen zum committen.'); + return false; + } + // Der Rest wie in commit-current-folder + let currentBranch = null; + try { + currentBranch = (await git.revparse(['--abbrev-ref', 'HEAD'])).trim(); + debug(`[autoCommit] Aktueller Branch: ${currentBranch}`); + } catch { + debug('[autoCommit] HEAD ist detached.'); + } + if (!currentBranch || currentBranch === 'HEAD') { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const backupBranch = `backup-master-${timestamp}`; + const branches = await git.branchLocal(); + if (branches.all.includes('master')) { + await git.branch(['-m', 'master', backupBranch]); + debug(`[autoCommit] Alter master in ${backupBranch} umbenannt.`); + } + await git.checkout(['-b', 'master']); + debug('[autoCommit] Neuer master-Branch erstellt und ausgecheckt.'); + } + await git.add(['-A']); + debug('[autoCommit] Alle Änderungen gestaged.'); + await git.commit(message || '[auto]'); + debug('[autoCommit] Commit erfolgreich erstellt.'); + return true; +} + + app.whenReady().then(() => { const win = createWindow(); @@ -118,22 +200,39 @@ app.whenReady().then(() => { ]); Menu.setApplicationMenu(menu); - // 1) Beim Start bereits gespeicherte Ordner überwachen - const folders = store.get('folders'); - folders.forEach(folder => { - // nur watchen, wenn .git existiert - if (fs.existsSync(path.join(folder, '.git', 'refs', 'heads', 'master'))) { - watchRepo(folder, win); + // 1) Beim Start bereits gespeicherte Ordner überwachen und monitoren + const folders = store.get('folders') || []; + folders.forEach(folderObj => { + if (fs.existsSync(path.join(folderObj.path, '.git', 'refs', 'heads', 'master'))) { + watchRepo(folderObj.path, win); + } + if (folderObj.monitoring) { + startMonitoringWatcher(folderObj.path, win); } }); // 2) IPC-Handler + ipcMain.handle('get-selected', () => { + const folders = store.get('folders') || []; + const selectedPath = store.get('selected'); + return folders.find(f => f.path === selectedPath) || null; + }); + ipcMain.handle('set-selected', (_e, folderObjOrPath) => { + // Akzeptiert sowohl String (legacy) als auch Objekt: + const folderPath = typeof folderObjOrPath === 'string' + ? folderObjOrPath + : folderObjOrPath.path; + store.set('selected', folderPath); + const folders = store.get('folders') || []; + return folders.find(f => f.path === folderPath) || null; + }); // Liste aller Folders ipcMain.handle('get-folders', () => store.get('folders')); - // Ordner hinzufügen: Open-Dialog, init, Store-Update, watchen + + // Ordner hinzufügen: Open-Dialog, init, Store-Update, watchen, monitoren ipcMain.handle('add-folder', async () => { const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openDirectory'] @@ -142,6 +241,24 @@ app.whenReady().then(() => { return store.get('folders'); } const newFolder = filePaths[0]; + await initGitRepo(newFolder); + let folders = store.get('folders') || []; + let folderObj = folders.find(f => f.path === newFolder); + if (!folderObj) { + folderObj = { path: newFolder, monitoring: true }; + folders.push(folderObj); + store.set('folders', folders); + } + store.set('selected', newFolder); + watchRepo(newFolder, win); + startMonitoringWatcher(newFolder, win); + return store.get('folders'); + }); +/* + ipcMain.handle('add-folder', async () => { + const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openDirectory'] }); + if (canceled || !filePaths[0]) return store.get('folders'); + const newFolder = filePaths[0]; // Repo initialisieren await initGitRepo(newFolder); @@ -158,8 +275,27 @@ app.whenReady().then(() => { return store.get('folders'); }); - +*/ // Ordner entfernen: Watcher schließen, Store-Update + ipcMain.handle('remove-folder', (_e, folderObj) => { + const folders = store.get('folders') || []; + const updated = folders.filter(f => f.path !== folderObj.path); + store.set('folders', updated); + if (store.get('selected') === folderObj.path) store.set('selected', null); + stopMonitoringWatcher(folderObj.path); + const watcher = repoWatchers.get(folderObj.path); + if (watcher) watcher.close(), repoWatchers.delete(folderObj.path); + return updated; + }); + +/* + ipcMain.handle('get-selected', () => store.get('selected')); + ipcMain.handle('set-selected', (_e, folderPath) => { + store.set('selected', folderPath); + return folderPath; + }); + */ + /* ipcMain.handle('remove-folder', (_e, folder) => { const watcher = repoWatchers.get(folder); if (watcher) { @@ -173,41 +309,45 @@ app.whenReady().then(() => { } return updated; }); + */ + // Zähle Commits - ipcMain.handle('get-commit-count', async (_e, folder) => { - const git = simpleGit(folder); + ipcMain.handle('get-commit-count', async (_e, folderObj) => { + const git = simpleGit(folderObj.path); const log = await git.log(); return log.total; // Anzahl der Commits }); // Prüfe, ob es ungestagte Änderungen gibt - ipcMain.handle('has-diffs', async (_e, folder) => { - const git = simpleGit(folder); + ipcMain.handle('has-diffs', async (_e, folderObj) => { + const git = simpleGit(folderObj.path); const status = await git.status(); // modified, not_added, deleted, etc. return status.files.length > 0; }); // Entferne das .git-Verzeichnis - ipcMain.handle('remove-git-folder', async (_e, folder) => { - const gitDir = path.join(folder, '.git'); + ipcMain.handle('remove-git-folder', async (_e, folderObj) => { + const gitDir = path.join(folderObj.path, '.git'); if (fs.existsSync(gitDir)) { await fs.promises.rm(gitDir, { recursive: true, force: true }); } return; }); +/* // Selected ipcMain.handle('get-selected', () => store.get('selected')); ipcMain.handle('set-selected', (_e, folder) => { store.set('selected', folder); return folder; }); + */ // Commits holen - ipcMain.handle('get-commits', async (_e, folder) => { - const git = simpleGit(folder); + ipcMain.handle('get-commits', async (_e, folderObj) => { + const git = simpleGit(folderObj.path); // alle Commits holen const log = await git.log(['--all']); // aktuellen HEAD‐Hash ermitteln @@ -224,40 +364,40 @@ app.whenReady().then(() => { }); // Diff - ipcMain.handle('diff-commit', async (_e, folder, hash) => { - const git = simpleGit(folder); + ipcMain.handle('diff-commit', async (_e, folderObj, hash) => { + const git = simpleGit(folderObj.path); return git.diff([`${hash}^!`]); }); // Revert - ipcMain.handle('revert-commit', async (_e, folder, hash) => { - const git = simpleGit(folder); + ipcMain.handle('revert-commit', async (_e, folderObj, hash) => { + const git = simpleGit(folderObj.path); await git.revert(hash, ['--no-edit']); }); /** * Checkt das Arbeitsverzeichnis auf exakt den Zustand von `hash` aus. */ - ipcMain.handle('checkout-commit', async (_e, folder, hash) => { - const git = simpleGit(folder); + ipcMain.handle('checkout-commit', async (_e, folderObj, hash) => { + const git = simpleGit(folderObj.path); // clean mode: alle lokalen Veränderungen verwerfen await git.checkout([hash, '--force']); }); // Snapshot - ipcMain.handle('snapshot-commit', async (_e, folder, hash) => { + ipcMain.handle('snapshot-commit', async (_e, folderObj, hash) => { const { canceled, filePaths } = await dialog.showOpenDialog({ title: 'Ordner auswählen zum Speichern des Snapshots', properties: ['openDirectory'] }); if (canceled || !filePaths[0]) return; const outDir = filePaths[0]; - const baseName = path.basename(folder); + const baseName = path.basename(folderObj.path); const filePath = path.join(outDir, `${baseName}-${hash}.zip`); return new Promise((resolve, reject) => { exec( - `git -C "${folder}" archive --format zip --output "${filePath}" ${hash}`, + `git -C "${folderObj.path}" archive --format zip --output "${filePath}" ${hash}`, err => err ? reject(err) : resolve(filePath) ); }); @@ -277,38 +417,58 @@ app.whenReady().then(() => { ipcMain.handle('set-skip-git-prompt', (_e,val) => store.set('skipGitPrompt', val)); - ipcMain.handle('commit-current-folder', async (_e, folder, message) => { + + + + ipcMain.handle('commit-current-folder', async (_e, folderObj, message) => { + folder = folderObj.path; try { debug(`Commit-Vorgang für ${folder} gestartet…`); const git = simpleGit(folder); - // 1) Branch‐Status prüfen + // Prüfe: Gibt es was zu committen? + const status = await git.status(); + if ( + status.not_added.length === 0 && + status.created.length === 0 && + status.deleted.length === 0 && + status.modified.length === 0 && + status.renamed.length === 0 + ) { + debug('Nichts zu committen.'); + return { success: false, error: 'Nichts zu committen.' }; + } + + // HEAD-Status prüfen let currentBranch = null; try { currentBranch = (await git.revparse(['--abbrev-ref', 'HEAD'])).trim(); debug(`Aktueller Branch: ${currentBranch}`); - } catch { + } catch (err) { debug('HEAD ist detached.'); } - // 2) Falls detached, alten master sichern und neuen anlegen + // Falls detached, **jetzt erst** alten Branch umbenennen und neuen master erzeugen if (!currentBranch || currentBranch === 'HEAD') { - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupBranch = `backup-master-${timestamp}`; - const branches = await git.branchLocal(); + // Alten master umbenennen (nur falls vorhanden!) + const branches = await git.branchLocal(); if (branches.all.includes('master')) { await git.branch(['-m', 'master', backupBranch]); - debug(`Alter master → ${backupBranch}`); + debug(`Alter master-Branch wurde in ${backupBranch} umbenannt.`); } + // Neuer master-Branch await git.checkout(['-b', 'master']); - debug('Neuer master-Branch erstellt.'); + debug('Neuer master-Branch erstellt und ausgecheckt.'); } - // 3) Commit & Push - await git.add(['-A']); debug('git add -A'); - await git.commit(message || 'test'); debug('git commit'); - await git.push(['-u','origin','master']); debug('git push'); + await git.add(['-A']); + debug('Alle Änderungen gestaged.'); + await git.commit(message || 'test'); + debug('Commit erfolgreich erstellt.'); + // Push hier ggf. noch auskommentiert lassen return { success: true }; } catch (err) { @@ -317,8 +477,21 @@ app.whenReady().then(() => { } }); - - + ipcMain.handle('set-monitoring', async (_e, folderPath, monitoring) => { + let folders = store.get('folders') || []; + folders = folders.map(f => + f.path === folderPath ? { ...f, monitoring } : f + ); + store.set('folders', folders); + debug(`[STORE] Monitoring für ${folderPath}: ${monitoring}`); + // Monitoring-Watcher starten/stoppen → gleich mehr dazu + if (monitoring) { + startMonitoringWatcher(folderPath, win); + } else { + stopMonitoringWatcher(folderPath); + } + return monitoring; + }); // … Ende der IPC-Handler … diff --git a/package.json b/package.json index ebc7386..a87eb79 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "dependencies": { "chokidar": "^4.0.3", "electron-store": "^8.2.0", + "ignore": "^7.0.4", "simple-git": "^3.20.0", "suncalc": "^1.9.0" }, diff --git a/preload.js b/preload.js index 74fd292..671aa3a 100644 --- a/preload.js +++ b/preload.js @@ -10,23 +10,20 @@ contextBridge.exposeInMainWorld('settingsAPI', { contextBridge.exposeInMainWorld('electronAPI', { getFolders: () => ipcRenderer.invoke('get-folders'), addFolder: () => ipcRenderer.invoke('add-folder'), - removeFolder: folder => ipcRenderer.invoke('remove-folder', folder), + removeFolder: folderObj=> ipcRenderer.invoke('remove-folder', folderObj), getSelected: () => ipcRenderer.invoke('get-selected'), - setSelected: folder => ipcRenderer.invoke('set-selected', folder), - listFolder: folder => ipcRenderer.invoke('list-folder', folder), - getCommits: folder => ipcRenderer.invoke('get-commits', folder), - diffCommit: (folder, hash) => ipcRenderer.invoke('diff-commit', folder, hash), - revertCommit: (folder, hash) => ipcRenderer.invoke('revert-commit', folder, hash), - snapshotCommit: (folder, hash) => ipcRenderer.invoke('snapshot-commit', folder, hash), - checkoutCommit: (folder, hash) => ipcRenderer.invoke('checkout-commit', folder, hash), - getCommitCount: folder => ipcRenderer.invoke('get-commit-count', folder), - hasDiffs: folder => ipcRenderer.invoke('has-diffs', folder), - removeGitFolder: folder => ipcRenderer.invoke('remove-git-folder', folder), - getSkipPrompt: () => ipcRenderer.invoke('get-skip-git-prompt'), - setSkipPrompt: val => ipcRenderer.invoke('set-skip-git-prompt', val), - showFolderContextMenu: folderPath => ipcRenderer.send('show-folder-context-menu', folderPath), - commitCurrentFolder: (folder, message) => ipcRenderer.invoke('commit-current-folder', folder, message), + setSelected: folderObj=> ipcRenderer.invoke('set-selected', folderObj), + getCommits: folderObj=> ipcRenderer.invoke('get-commits', folderObj), + diffCommit: (folderObj, hash) => ipcRenderer.invoke('diff-commit', folderObj, hash), + revertCommit: (folderObj, hash) => ipcRenderer.invoke('revert-commit', folderObj, hash), + snapshotCommit: (folderObj, hash) => ipcRenderer.invoke('snapshot-commit', folderObj, hash), + checkoutCommit: (folderObj, hash) => ipcRenderer.invoke('checkout-commit', folderObj, hash), + getCommitCount: folderObj=> ipcRenderer.invoke('get-commit-count', folderObj), + hasDiffs: folderObj=> ipcRenderer.invoke('has-diffs', folderObj), + removeGitFolder:folderObj=> ipcRenderer.invoke('remove-git-folder', folderObj), + commitCurrentFolder: (folderObj, message) => ipcRenderer.invoke('commit-current-folder', folderObj, message), showFolderContextMenu: folderPath => ipcRenderer.send('show-folder-context-menu', folderPath), + setMonitoring: (folderObj, monitoring) => ipcRenderer.invoke('set-monitoring', folderObj.path, monitoring), }); ipcRenderer.on('repo-updated', (_e, folder) => { diff --git a/renderer.js b/renderer.js index bb8267e..53740dc 100644 --- a/renderer.js +++ b/renderer.js @@ -8,18 +8,13 @@ window.addEventListener('DOMContentLoaded', async () => { const contentList = document.getElementById('contentList'); const panel = document.querySelector('.flex-1.p-4.overflow-y-auto'); - - - // 1) Baby-Blau und Nacht-Blau als RGB-Arrays + // Farben für Sky-Mode const DAY_COLOR = [173, 216, 230]; const NIGHT_COLOR = [0, 0, 50]; - // 2) Linearer Interpolator function lerpColor(c1, c2, t) { return c1.map((v, i) => Math.round(v + t * (c2[i] - v))); } - - // 3) Entscheider für den Zeit-Factor function getTimeFactor() { const now = new Date(); const md = now.getHours() * 60 + now.getMinutes(); @@ -29,29 +24,20 @@ window.addEventListener('DOMContentLoaded', async () => { if (md < 20 * 60) return 1 - ((md - 16*60) / (4*60)); return 0; } - - // 4) setzt die Hintergrundfarbe function updateBackground() { const factor = getTimeFactor(); const [r,g,b] = lerpColor(NIGHT_COLOR, DAY_COLOR, factor); panel.style.backgroundColor = `rgb(${r}, ${g}, ${b})`; } - - // 5) applySkyMode-Funktion let skyIntervalId, titleIntervalId; function applySkyMode(enabled) { document.body.classList.toggle('sky-mode', enabled); - - // alte Intervalle löschen clearInterval(skyIntervalId); clearInterval(titleIntervalId); if (enabled) { - // Hintergrund updaten updateBackground(); skyIntervalId = setInterval(updateBackground, 60_000); - - // Titel-Farbe je nach Uhrzeit function updateTitleColor() { const hour = new Date().getHours(); if (hour >= 18 || hour < 6) { @@ -63,124 +49,129 @@ window.addEventListener('DOMContentLoaded', async () => { updateTitleColor(); titleIntervalId = setInterval(updateTitleColor, 60_000); } else { - // Sky-Mode aus → zurücksetzen panel.style.backgroundColor = ''; - titleEl.style.color = ''; + titleEl.style.color = ''; } } - - // 6) initial anwenden und auf Event lauschen const initialSky = await window.settingsAPI.getSkyMode(); applySkyMode(initialSky); window.addEventListener('skymode-changed', e => applySkyMode(e.detail)); - // … restliches Rendering wie gehabt … function basename(fullPath) { return fullPath.replace(/.*[\\/]/, ''); } -async function renderSidebar() { - const folders = await window.electronAPI.getFolders(); - const selected = await window.electronAPI.getSelected(); - folderList.innerHTML = ''; + // Utility für FolderObj-Suche per Path + async function getFolderObjByPath(path) { + const folders = await window.electronAPI.getFolders(); + return folders.find(f => f.path === path) || null; + } - folders.forEach(folder => { - // 1) li anlegen, selected-Klasse statt bg-[#fecdd3] - const li = document.createElement('li'); - li.className = [ - 'flex items-center justify-between px-3 py-2 rounded cursor-pointer', - folder === selected ? 'selected' : '' - ].join(' '); + async function renderSidebar() { + const folders = await window.electronAPI.getFolders(); + const selected = await window.electronAPI.getSelected(); + folderList.innerHTML = ''; - // 2) inneres Markup, ohne Hard-Coded-Farben - li.innerHTML = ` -