const { app, BrowserWindow, Tray, Menu, nativeImage, ipcMain, Notification, dialog, shell } = require('electron'); const path = require('path'); const https = require('https'); const fs = require('fs'); const os = require('os'); // ─── NOM DE L'APPLICATION ───────────────────────────────────────────────────── app.setName('Minuteur Dragodinde'); // Windows utilise l'AppUserModelId pour le nom affiché dans les notifications if (process.platform === 'win32') { app.setAppUserModelId('Minuteur Dragodinde'); } // ─── MODE DEV ──────────────────────────────────────────────────────────────── // En dev (npm start), les données sont isolées de l'app installée if (!app.isPackaged) { app.setPath('userData', path.join(app.getPath('appData'), 'MinuteurDragodinde-DEV')); } // ─── CONFIG GITEA ───────────────────────────────────────────────────────────── const GITEA_HOST = 'gitea.mickael-pol.fr'; // ton instance Gitea const GITEA_USER = 'mickael'; // ton user Gitea const GITEA_REPO = 'dd-timer'; // ton repo const CURRENT_VERSION = app.getVersion(); // lu depuis package.json let mainWindow; let tray; let isQuitting = false; let updateInfo = null; // { version, downloadUrl } si mise à jour dispo let downloading = false; // ─── ICÔNE ─────────────────────────────────────────────────────────────────── function getTrayIcon() { const fs = require('fs'); const iconPath = path.join(__dirname, 'icon.png'); if (fs.existsSync(iconPath)) return nativeImage.createFromPath(iconPath); return nativeImage.createEmpty(); } // ─── FENÊTRE ───────────────────────────────────────────────────────────────── function createWindow() { mainWindow = new BrowserWindow({ width: 1280, height: 900, minWidth: 960, minHeight: 650, title: 'Minuteur Dragodinde - Dofus 3', backgroundColor: '#0b0b14', webPreferences: { nodeIntegration: false, contextIsolation: true, backgroundThrottling: false, preload: path.join(__dirname, 'preload.js'), }, }); mainWindow.loadFile(path.join(__dirname, 'src', 'index.html')); mainWindow.on('close', (e) => { if (!isQuitting) { e.preventDefault(); mainWindow.hide(); if (tray) tray.displayBalloon({ iconType: 'info', title: 'Minuteur Dragodinde', content: "Tourne en arriere-plan. Les alarmes sonneront normalement.", }); } }); // Envoyer les infos de version au renderer une fois chargé mainWindow.webContents.on('did-finish-load', () => { mainWindow.webContents.send('app-version', CURRENT_VERSION); if (updateInfo) { mainWindow.webContents.send('update-available', updateInfo); } // Badge DEV visible dans l'interface if (!app.isPackaged) { mainWindow.webContents.executeJavaScript(` const p = document.querySelector('header p'); if (p && !document.getElementById('dev-badge')) { const b = document.createElement('span'); b.id = 'dev-badge'; b.textContent = 'DEV'; b.style.cssText = 'background:#ff9820;color:#000;padding:2px 10px;border-radius:8px;font-size:0.72rem;font-weight:800;margin-left:8px;vertical-align:middle'; p.appendChild(b); } `).catch(()=>{}); } }); } // ─── TRAY ───────────────────────────────────────────────────────────────────── function createTray() { tray = new Tray(getTrayIcon()); tray.setToolTip(`Minuteur Dragodinde v${CURRENT_VERSION}`); rebuildTrayMenu(); tray.on('double-click', () => { mainWindow.show(); mainWindow.focus(); }); } function rebuildTrayMenu() { if (!tray) return; const items = [ { label: `Minuteur Dragodinde v${CURRENT_VERSION}`, enabled: false }, { type: 'separator' }, { label: 'Ouvrir', click: () => { mainWindow.show(); mainWindow.focus(); } }, ]; if (updateInfo) { items.push({ type: 'separator' }); items.push({ label: `⬆ Mise a jour v${updateInfo.version} disponible !`, click: () => startDownload(), }); } items.push({ type: 'separator' }); items.push({ label: 'Quitter', click: () => { isQuitting = true; app.quit(); } }); tray.setContextMenu(Menu.buildFromTemplate(items)); } // ─── NOTIFICATIONS ──────────────────────────────────────────────────────────── function fireNotification(title, body) { if (!Notification.isSupported()) return; const n = new Notification({ title, body, timeoutType: 'never' }); n.on('click', () => { mainWindow.show(); mainWindow.focus(); }); n.show(); } // ─── IPC ────────────────────────────────────────────────────────────────────── ipcMain.on('trigger-alarm', (event, { enclosName }) => { fireNotification('Dragodindes pretes !', enclosName + ' - Toutes les cibles ont ete atteintes !'); if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('play-alarm-sound'); } }); ipcMain.on('show-notification', (event, { title, body }) => { fireNotification(title, body); }); // ─── NTFY (notifications mobiles) ───────────────────────────────────────── ipcMain.on('send-ntfy', (event, { url, title, message }) => { if (!url) return; try { const parsed = new URL(url.trim()); const mod = parsed.protocol === 'https:' ? https : require('http'); const postData = message; const options = { hostname: parsed.hostname, port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80), path: parsed.pathname + parsed.search, method: 'POST', headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': Buffer.byteLength(postData, 'utf-8'), 'Title': title, 'Priority': 'high', 'Tags': 'hatching_chick', }, }; const req = mod.request(options, (res) => { res.on('data', () => {}); // drain res.on('end', () => {}); }); req.on('error', (e) => console.warn('ntfy send error:', e.message)); req.write(postData, 'utf-8'); req.end(); } catch (e) { console.warn('ntfy error:', e.message); } }); ipcMain.on('focus-window', () => { mainWindow.show(); mainWindow.focus(); }); // Renderer demande à installer la mise à jour ipcMain.on('install-update', () => startDownload()); // Renderer demande la version ipcMain.handle('get-version', () => CURRENT_VERSION); // ─── VÉRIFICATION DE MISE À JOUR ───────────────────────────────────────────── function compareVersions(a, b) { // Retourne > 0 si b > a (b est plus récent) const pa = a.replace(/^v/, '').split('.').map(Number); const pb = b.replace(/^v/, '').split('.').map(Number); for (let i = 0; i < 3; i++) { if ((pb[i] || 0) > (pa[i] || 0)) return 1; if ((pb[i] || 0) < (pa[i] || 0)) return -1; } return 0; } function checkForUpdates(silent = false) { // API Gitea : GET /api/v1/repos/{user}/{repo}/releases?limit=1 // Retourne un tableau — le premier élément est la release la plus récente const options = { hostname: GITEA_HOST, port: 443, path: `/api/v1/repos/${GITEA_USER}/${GITEA_REPO}/releases?limit=1`, method: 'GET', headers: { 'User-Agent': `MinuteurDragodinde/${CURRENT_VERSION}`, 'Accept': 'application/json', }, }; const req = https.request(options, (res) => { let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { try { const releases = JSON.parse(data); // Gitea renvoie un tableau, on prend le premier (le plus récent) if (!Array.isArray(releases) || releases.length === 0) return; const release = releases[0]; const latestVersion = release.tag_name; if (!latestVersion) return; if (compareVersions(CURRENT_VERSION, latestVersion) > 0) { // Chercher l'asset installeur (.exe contenant "Setup") const asset = release.assets && release.assets.find(a => a.name.includes('Setup') && a.name.endsWith('.exe') ); if (!asset) return; updateInfo = { version: latestVersion, downloadUrl: asset.browser_download_url, assetName: asset.name, releaseNotes: release.body || '', }; if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('update-available', updateInfo); } fireNotification( `Mise a jour v${latestVersion} disponible !`, 'Cliquez pour mettre a jour Minuteur Dragodinde.' ); rebuildTrayMenu(); } else if (!silent) { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('update-not-available'); } } } catch (e) { console.error('Update check parse error:', e.message); } }); }); req.on('error', (e) => console.error('Update check error:', e.message)); req.end(); } // ─── TÉLÉCHARGEMENT ET REMPLACEMENT ────────────────────────────────────────── function startDownload() { if (!updateInfo || downloading) return; downloading = true; if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('update-downloading', { version: updateInfo.version }); } const tmpDir = os.tmpdir(); const tmpExe = path.join(tmpDir, updateInfo.assetName); const file = fs.createWriteStream(tmpExe); const currentExe = process.execPath; // Suivre les redirections manuellement (GitHub assets redirigent) function download(url, redirectCount = 0) { if (redirectCount > 5) { sendUpdateError('Trop de redirections.'); return; } const urlObj = new URL(url); const mod = urlObj.protocol === 'https:' ? https : require('http'); const opts = { hostname: urlObj.hostname, path: urlObj.pathname + urlObj.search, method: 'GET', headers: { 'User-Agent': `MinuteurDragodinde/${CURRENT_VERSION}` }, }; mod.request(opts, (res) => { if (res.statusCode === 302 || res.statusCode === 301) { download(res.headers.location, redirectCount + 1); return; } if (res.statusCode !== 200) { sendUpdateError(`Erreur HTTP ${res.statusCode}`); return; } const total = parseInt(res.headers['content-length'] || '0', 10); let received = 0; res.on('data', chunk => { received += chunk.length; file.write(chunk); if (total > 0 && mainWindow && !mainWindow.isDestroyed()) { const pct = Math.round((received / total) * 100); mainWindow.webContents.send('update-progress', { percent: pct }); } }); res.on('end', () => { file.end(); file.on('finish', () => { // Lancer le script de remplacement launchUpdater(tmpExe, currentExe); }); }); res.on('error', e => sendUpdateError(e.message)); }).on('error', e => sendUpdateError(e.message)).end(); } download(updateInfo.downloadUrl); } function sendUpdateError(msg) { downloading = false; console.error('Update error:', msg); if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('update-error', { message: msg }); } } function launchUpdater(newExe, currentExe) { // Pour un installeur NSIS : on le lance directement avec /S pour silent install // L'installeur gère lui-même le remplacement de l'ancienne version if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('update-ready'); } const { spawn } = require('child_process'); // Petit délai pour laisser le message s'afficher setTimeout(() => { // Lancer l'installeur NSIS en mode silencieux // /S = silent, /D= permet de spécifier le dossier d'installation spawn(newExe, ['/S'], { detached: true, stdio: 'ignore', windowsHide: false, // L'installeur peut avoir besoin d'être visible pour UAC }).unref(); isQuitting = true; app.quit(); }, 1500); } // ─── CYCLE DE VIE ──────────────────────────────────────────────────────────── app.whenReady().then(() => { createWindow(); createTray(); // Vérifier les mises à jour au démarrage (silencieux) setTimeout(() => checkForUpdates(true), 3000); // Revérifier toutes les heures setInterval(() => checkForUpdates(true), 60 * 60 * 1000); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { if (process.platform === 'darwin') app.quit(); }); app.on('before-quit', () => { isQuitting = true; });