diff --git a/main.js b/main.js index 2376ca0..956f5e3 100644 --- a/main.js +++ b/main.js @@ -1,5 +1,5 @@ // main.js — Main-Process with concurrent folder upload/download, progress events, and temp-dir cleanup -const { app, BrowserWindow, ipcMain, dialog, Menu, nativeImage } = require('electron'); +const { app, BrowserWindow, ipcMain, dialog, Menu, nativeImage, Tray, shell, clipboard } = require('electron'); const ppath = require('path'); const fs = require('fs'); const os = require('os'); @@ -15,6 +15,7 @@ const { createRepoGitea, checkGiteaConnection, listGiteaRepos, + getGiteaUserHeatmap, getGiteaRepoContents, getGiteaFileContent, uploadGiteaFile, @@ -254,13 +255,32 @@ function getSafeTmpDir(baseName) { /* ----------------------------- app / window ----------------------------- */ +let tray = null; + +function createTray(win) { + const iconPath = ppath.join(__dirname, 'renderer', 'icon.png'); + tray = new Tray(nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 })); + tray.setToolTip('Git Manager Explorer Pro'); + const menu = Menu.buildFromTemplate([ + { label: 'Öffnen', click: () => { win.show(); win.focus(); } }, + { type: 'separator' }, + { label: 'Beenden', click: () => { app.quit(); } } + ]); + tray.setContextMenu(menu); + tray.on('double-click', () => { win.show(); win.focus(); }); +} + function createWindow() { // Entfernt das Menü (File, Edit, View...) komplett Menu.setApplicationMenu(null); + const startHidden = process.argv.includes('--hidden'); + const win = new BrowserWindow({ width: 1200, height: 820, + frame: false, + show: !startHidden, webPreferences: { preload: ppath.join(__dirname, 'preload.js'), nodeIntegration: false, @@ -269,6 +289,17 @@ function createWindow() { }); win.loadFile(ppath.join(__dirname, 'renderer', 'index.html')); // win.webContents.openDevTools(); + + createTray(win); + + // Schließen-Button -> Tray statt Beenden (nur wenn Autostart aktiv) + win.on('close', (e) => { + const { enabled } = app.getLoginItemSettings(); + if (enabled) { + e.preventDefault(); + win.hide(); + } + }); } app.whenReady().then(() => { @@ -804,6 +835,21 @@ ipcMain.handle('list-gitea-repos', async (event, data) => { } }); +ipcMain.handle('get-gitea-user-heatmap', async (event, data) => { + try { + const credentials = readCredentials(); + const token = (data && data.token) || (credentials && credentials.giteaToken); + const url = (data && data.url) || (credentials && credentials.giteaURL); + if (!token || !url) return { ok: false, error: 'missing-token-or-url' }; + + const result = await getGiteaUserHeatmap({ token, url }); + return { ok: true, ...result }; + } catch (e) { + console.error('get-gitea-user-heatmap error', e); + return { ok: false, error: mapIpcError(e) }; + } +}); + ipcMain.handle('get-gitea-repo-contents', async (event, data) => { try { const credentials = readCredentials(); @@ -2349,6 +2395,55 @@ ipcMain.handle('save-recent', async (event, recent) => { // main.js - Updater IPC Handlers +// Window Controls +ipcMain.on('window-minimize', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) win.minimize(); +}); +ipcMain.on('window-maximize', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { win.isMaximized() ? win.unmaximize() : win.maximize(); } +}); +ipcMain.on('window-close', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) win.close(); +}); + +// Autostart +ipcMain.handle('set-autostart', (event, enable) => { + app.setLoginItemSettings({ + openAtLogin: enable, + openAsHidden: true, + args: ['--hidden'] + }); + return { ok: true }; +}); + +ipcMain.handle('get-autostart', () => { + const settings = app.getLoginItemSettings({ args: ['--hidden'] }); + return { ok: true, enabled: settings.openAtLogin }; +}); + +ipcMain.handle('copy-to-clipboard', async (_event, text) => { + try { + clipboard.writeText(String(text || '')); + return { ok: true }; + } catch (e) { + return { ok: false, error: String(e) }; + } +}); + +ipcMain.handle('open-external-url', async (_event, rawUrl) => { + try { + const url = String(rawUrl || '').trim(); + if (!url) return { ok: false, error: 'Leere URL' }; + await shell.openExternal(url); + return { ok: true }; + } catch (e) { + return { ok: false, error: String(e) }; + } +}); + // 1. Version abfragen ipcMain.handle('get-app-version', async () => { return { ok: true, version: app.getVersion() }; @@ -2407,4 +2502,106 @@ ipcMain.handle('test-gitea-connection', async (event, data) => { console.error('test-gitea-connection error', e); return { ok: false, error: mapIpcError(e) }; } +}); + +ipcMain.handle('validate-repo-name', async (_event, data) => { + try { + const name = String(data && data.name || '').trim(); + const platform = String(data && data.platform || 'gitea').trim().toLowerCase(); + const validPattern = /^[a-zA-Z0-9](?:[a-zA-Z0-9._-]{0,98}[a-zA-Z0-9])?$/; + const validFormat = validPattern.test(name); + + if (!name) { + return { ok: true, validFormat: false, existsExact: false, similar: [], checked: false, reason: 'empty-name' }; + } + + if (!validFormat) { + return { ok: true, validFormat: false, existsExact: false, similar: [], checked: false, reason: 'invalid-format' }; + } + + if (platform !== 'gitea') { + return { ok: true, validFormat: true, existsExact: false, similar: [], checked: false, reason: 'platform-not-supported' }; + } + + const credentials = readCredentials(); + if (!credentials || !credentials.giteaToken || !credentials.giteaURL) { + return { ok: true, validFormat: true, existsExact: false, similar: [], checked: false, reason: 'missing-credentials' }; + } + + const list = await listGiteaRepos({ token: credentials.giteaToken, url: credentials.giteaURL }); + const repos = Array.isArray(list) ? list : []; + const allNames = repos + .map(r => String(r && r.name || '').trim()) + .filter(Boolean); + + const lower = name.toLowerCase(); + const normalized = lower.replace(/[\s._-]+/g, ''); + const existsExact = allNames.some(n => n.toLowerCase() === lower); + + const similar = allNames + .filter(n => { + const nLower = n.toLowerCase(); + const nNorm = nLower.replace(/[\s._-]+/g, ''); + return nLower.includes(lower) || lower.includes(nLower) || nNorm.includes(normalized) || normalized.includes(nNorm); + }) + .filter(n => n.toLowerCase() !== lower) + .slice(0, 8); + + return { + ok: true, + validFormat: true, + existsExact, + similar, + checked: true, + totalKnown: allNames.length + }; + } catch (e) { + return { ok: false, error: String(e) }; + } +}); + +ipcMain.handle('check-clone-target-collisions', async (_event, data) => { + try { + const targetDir = String(data && data.targetDir || '').trim(); + const rawRepos = Array.isArray(data && data.repos) ? data.repos : []; + const repos = rawRepos + .map(v => String(v || '').trim()) + .filter(Boolean) + .map(v => { + const parts = v.split('/'); + const repo = (parts[1] || '').trim(); + return { input: v, repo }; + }) + .filter(r => r.repo); + + const duplicateRepoNames = []; + const seen = new Map(); + for (const item of repos) { + const key = item.repo.toLowerCase(); + const count = (seen.get(key) || 0) + 1; + seen.set(key, count); + } + for (const [name, count] of seen.entries()) { + if (count > 1) duplicateRepoNames.push(name); + } + + const existingTargets = []; + if (targetDir) { + for (const item of repos) { + const repoDir = ppath.join(targetDir, item.repo); + if (fs.existsSync(repoDir)) { + existingTargets.push(repoDir); + } + } + } + + return { + ok: true, + duplicateRepoNames, + existingTargets, + hasCollisions: duplicateRepoNames.length > 0 || existingTargets.length > 0 + }; + } catch (e) { + return { ok: false, error: String(e) }; + } }); \ No newline at end of file