dd-timer/main.js

412 lines
15 KiB
JavaScript

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();
const choice = dialog.showMessageBoxSync(mainWindow, {
type: 'question',
buttons: ['Minimiser', 'Quitter'],
defaultId: 0,
cancelId: 0,
title: 'Minuteur Dragodinde',
message: 'Que souhaites-tu faire ?',
detail: 'Minimiser garde l\'app en arriere-plan.\nLes alarmes continueront de sonner.',
});
if (choice === 1) {
isQuitting = true;
app.quit();
} else {
mainWindow.hide();
}
}
});
// 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);
});
// ─── SAUVEGARDE FICHIER (persistante entre mises à jour) ─────────────────
const dataFile = path.join(app.getPath('userData'), 'dd-timer-data.json');
ipcMain.handle('load-data', () => {
try {
if (fs.existsSync(dataFile)) return fs.readFileSync(dataFile, 'utf-8');
} catch (e) { console.error('load-data error:', e.message); }
return null;
});
ipcMain.on('save-data', (event, json) => {
try {
fs.writeFileSync(dataFile, json, 'utf-8');
} catch (e) { console.error('save-data error:', e.message); }
});
// ─── 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) {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-ready');
}
const { spawn } = require('child_process');
// Script batch qui survit à la fermeture de l'app :
// 1. Attend que l'app se ferme
// 2. Lance l'installeur en silencieux
// 3. Relance l'app
// 4. Se supprime lui-même
const batPath = path.join(os.tmpdir(), 'dd-timer-update.cmd');
const batContent = [
'@echo off',
'timeout /t 3 /nobreak >nul',
`start /wait "" "${newExe}" /S`,
'timeout /t 5 /nobreak >nul',
`start "" "${currentExe}"`,
'del "%~f0"',
].join('\r\n');
fs.writeFileSync(batPath, batContent, 'utf-8');
setTimeout(() => {
spawn('cmd.exe', ['/c', batPath], {
detached: true,
stdio: 'ignore',
windowsHide: true,
}).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; });