Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

7 changed files with 133 additions and 1336 deletions

View File

@ -1,55 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Minuteur Dragodinde is a Windows desktop app (Electron) for managing Dragodinde breeding timers in Dofus 3. French-language UI.
## Commands
- **Dev**: `npm start` (runs `electron .`)
- **Build**: `npm run build` (produces NSIS installer in `dist/`)
- **Build scripts**: `build.bat` (auto-elevates to admin) or `build.ps1` for Windows
No test framework or linter is configured.
## Architecture
**Single-page Electron app with no bundler.** All source code lives in 4 files:
- `main.js` — Electron main process: window management, system tray, native notifications, ntfy push notifications (mobile), auto-update via Gitea Releases API, IPC handlers
- `preload.js` — Context bridge exposing `window.electronAPI` (alarm, notifications, ntfy, version, update channels)
- `src/index.html`**Monolithic ~2200-line file** containing all HTML, CSS, and JS in one file. This is the entire renderer process.
- `icon.png` — App icon (256x256)
### Renderer architecture (src/index.html)
All application logic is in `<script>` tags inside index.html. Key concepts:
- **State object `S`**: Central state with `enclos` array, `activeId`, settings. Persisted to `localStorage`.
- **Enclos**: Up to 6 independent pens, each holding up to 10 Dragodindes (DDs). Enclos and DDs are drag-and-drop reorderable.
- **Gauges**: 6 gauge types (baffeur, caresseur, foudroyeur, abreuvoir, dragofesse, mangeoire/XP) with tier-based progression rates (tiers 1-4 based on gauge level thresholds at 40k/70k/90k).
- **Timer system**: Per-enclos timers with snapshot-based calculation. `elapsed()` computes time since start, `gaugeAfter()`/`gainedIn()`/`timeToGain()` compute gauge progression.
- **Rendering**: Imperative DOM manipulation via `render()` function — no framework.
- **Dashboard/Stats views**: Special `activeId` values (`'dashboard'`, `'stats'`) for overview and statistics pages.
### IPC channels
Main→Renderer: `app-version`, `play-alarm-sound`, `update-available`, `update-downloading`, `update-progress`, `update-ready`, `update-error`
Renderer→Main: `trigger-alarm`, `show-notification`, `send-ntfy`, `focus-window`, `install-update`, `get-version`
### Auto-update flow
Checks Gitea Releases API on startup (after 3s) and hourly. Downloads NSIS Setup .exe to temp, launches with `/S` (silent), then quits app.
### Dev vs packaged mode
When `!app.isPackaged`, userData is stored in `MinuteurDragodinde-DEV` (isolated from installed app) and a DEV badge is injected into the UI.
## Key conventions
- All UI text is in French
- No external JS dependencies in renderer — everything is vanilla JS
- CSS uses custom properties defined in `:root` (color theme: dark purple/gaming aesthetic)
- Electron config: `contextIsolation: true`, `nodeIntegration: false`, `backgroundThrottling: false`

View File

@ -11,11 +11,10 @@ Application desktop Windows construite avec Electron.
- ⏱ **Timer en temps réel** avec calcul automatique par tier de jauge (1→4) - ⏱ **Timer en temps réel** avec calcul automatique par tier de jauge (1→4)
- 📊 **Dashboard** vue d'ensemble multi-enclos - 📊 **Dashboard** vue d'ensemble multi-enclos
- 🔔 **Notifications natives Windows** même application en arrière-plan - 🔔 **Notifications natives Windows** même application en arrière-plan
- 🔊 **4 sons d'alarme** au choix (Arpège, Pulsation, Fanfare, Cloche) - 🔊 **5 sons d'alarme** au choix (Arpège, Pulsation, Fanfare, Sirène, Cloche)
- 🐉 **Jauges** : Baffeur, Caresseur, Foudroyeur, Abreuvoir, Dragofesse, Mangeoire (XP) - 🐉 **Jauges** : Baffeur, Caresseur, Foudroyeur, Abreuvoir, Dragofesse, Mangeoire (XP)
- 🖱 **Drag & drop** des enclos et des Dragodindes pour les réordonner - 🖱 **Drag & drop** des enclos et des Dragodindes pour les réordonner
- ⬆ **Mise à jour automatique** via Gitea Releases - ⬆ **Mise à jour automatique** via Gitea Releases
- 📱 **Notifications mobiles** via ntfy (serveur self-hosted)
- 💾 Sauvegarde automatique locale - 💾 Sauvegarde automatique locale
## Installation (utilisateurs) ## Installation (utilisateurs)
@ -70,31 +69,63 @@ dd-timer/
## Changelog ## Changelog
### v1.1.2 ### v1.0.0
- 💾 **Sauvegarde persistante** — les donnees sont maintenant sauvegardees dans un fichier JSON, plus de perte de statistiques apres une mise a jour - Version initiale
- ❌ **Dialogue de fermeture** — cliquer sur la croix propose de minimiser en arriere-plan ou de quitter completement
### v1.1.1 Outil de gestion d'élevage de Dragodindes pour Dofus 3.
- 🔧 Correction de la mise a jour automatique — l'app se relance maintenant toute seule apres installation Application desktop Windows construite avec Electron.
- 🔧 Correction de l'affichage du titre des notifications ntfy (encodage base64 supprime)
### v1.1.0 ## Fonctionnalités
- 📱 **Notifications mobiles (ntfy)** — Alerte sur telephone quand un enclos est pret, via serveur ntfy self-hosted
- Modale de configuration avec QR code pour installer l'app ntfy (Play Store / App Store) - 🐦 Gestion de **6 enclos indépendants** avec jusqu'à 10 Dragodindes chacun
- QR code d'abonnement automatique via page de redirection (ntfy-redirect) - ⏱ **Timer en temps réel** avec calcul automatique par tier de jauge (1→4)
- Bouton de test des notifications - 📊 **Dashboard** vue d'ensemble multi-enclos
- Envoi via le processus principal Electron (pas de CORS) - 🔔 **Notifications natives Windows** même application en arrière-plan
- 🐣 **Systeme de bebes** — Ajout de bebes dragodindes issus de la reproduction dans chaque enclos - 🔊 **5 sons d'alarme** au choix (Arpège, Pulsation, Fanfare, Sirène, Cloche)
- Modale de selection par generation et race avec images - 🐉 **Jauges** : Baffeur, Caresseur, Foudroyeur, Abreuvoir, Dragofesse, Mangeoire (XP)
- Historique des bebes par enclos - 🖱 **Drag & drop** des enclos et des Dragodindes pour les réordonner
- 📊 **Onglet Statistiques** — Vue globale de l'elevage avec KPIs, repartition par race, et progression par enclos - 💾 Sauvegarde automatique locale
- 🖱 **Drag & drop** des onglets d'enclos pour les reordonner
- 🐉 **Images des dragodindes** par race avec couleurs par generation ## Installation (utilisateurs)
- 📝 **Sous-onglets par enclos** (Elevage / Historique bebes)
- 🔧 **Mode DEV** — Donnees isolees et badge DEV visible quand lance avec `npm start` 1. Télécharger `Minuteur Dragodinde Setup x.x.x.exe` depuis la section [Releases](../../releases)
- ⬆ **Mise a jour automatique** via Gitea Releases avec banniere de progression dans l'interface 2. **Clic droit → Propriétés → cocher "Débloquer" → OK** (important, une seule fois)
- 🔧 Correction de l'identifiant applicatif (`fr.mickael-pol.minuteur-dragodinde`) 3. Double-cliquer pour lancer l'installation → suivre l'assistant
- 🔧 Masquage des spinners natifs sur les champs numeriques 4. L'app apparaît dans le menu Démarrer et sur le Bureau
> **Si Windows affiche "Application inconnue"** : cliquer **"Informations complémentaires" → "Exécuter quand même"**
> **Si Smart App Control bloque** : Sécurité Windows → Contrôle des applications → Smart App Control → Évaluation, puis retélécharger
## Build (développeurs)
### Prérequis
- [Node.js LTS](https://nodejs.org)
### Compiler l'application
```bash
# Double-cliquer sur build.bat (se relance automatiquement en admin)
# ou manuellement :
npm install
npm run build
```
L'exécutable est généré dans `dist/`.
## Structure du projet
```
dd-timer/
├── src/
│ └── index.html # Interface complète (HTML/CSS/JS)
├── main.js # Processus principal Electron
├── preload.js # Pont sécurisé Electron ↔ renderer
├── icon.png # Icône application (256x256)
├── package.json # Config et dépendances
└── build.bat # Script de build Windows
```
## Changelog
### v1.0.0 ### v1.0.0
- Version initiale - Version initiale

117
main.js
View File

@ -4,19 +4,6 @@ const https = require('https');
const fs = require('fs'); const fs = require('fs');
const os = require('os'); 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 ───────────────────────────────────────────────────────────── // ─── CONFIG GITEA ─────────────────────────────────────────────────────────────
const GITEA_HOST = 'gitea.mickael-pol.fr'; // ton instance Gitea const GITEA_HOST = 'gitea.mickael-pol.fr'; // ton instance Gitea
const GITEA_USER = 'mickael'; // ton user Gitea const GITEA_USER = 'mickael'; // ton user Gitea
@ -55,21 +42,12 @@ function createWindow() {
mainWindow.on('close', (e) => { mainWindow.on('close', (e) => {
if (!isQuitting) { if (!isQuitting) {
e.preventDefault(); e.preventDefault();
const choice = dialog.showMessageBoxSync(mainWindow, { mainWindow.hide();
type: 'question', if (tray) tray.displayBalloon({
buttons: ['Minimiser', 'Quitter'], iconType: 'info',
defaultId: 0,
cancelId: 0,
title: 'Minuteur Dragodinde', title: 'Minuteur Dragodinde',
message: 'Que souhaites-tu faire ?', content: "Tourne en arriere-plan. Les alarmes sonneront normalement.",
detail: 'Minimiser garde l\'app en arriere-plan.\nLes alarmes continueront de sonner.',
}); });
if (choice === 1) {
isQuitting = true;
app.quit();
} else {
mainWindow.hide();
}
} }
}); });
@ -79,19 +57,6 @@ function createWindow() {
if (updateInfo) { if (updateInfo) {
mainWindow.webContents.send('update-available', 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(()=>{});
}
}); });
} }
@ -142,54 +107,6 @@ ipcMain.on('show-notification', (event, { title, body }) => {
fireNotification(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(); }); ipcMain.on('focus-window', () => { mainWindow.show(); mainWindow.focus(); });
// Renderer demande à installer la mise à jour // Renderer demande à installer la mise à jour
@ -353,34 +270,22 @@ function sendUpdateError(msg) {
} }
function launchUpdater(newExe, currentExe) { 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()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-ready'); mainWindow.webContents.send('update-ready');
} }
const { spawn } = require('child_process'); const { spawn } = require('child_process');
// Script batch qui survit à la fermeture de l'app : // Petit délai pour laisser le message s'afficher
// 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(() => { setTimeout(() => {
spawn('cmd.exe', ['/c', batPath], { // Lancer l'installeur NSIS en mode silencieux
// /S = silent, /D= permet de spécifier le dossier d'installation
spawn(newExe, ['/S'], {
detached: true, detached: true,
stdio: 'ignore', stdio: 'ignore',
windowsHide: true, windowsHide: false, // L'installeur peut avoir besoin d'être visible pour UAC
}).unref(); }).unref();
isQuitting = true; isQuitting = true;

View File

@ -1,74 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Minuteur Dragodinde - Notifications</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0b0b14;color:#dddaf8;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
.card{background:#181828;border:1px solid #2a2a45;border-radius:16px;padding:32px;max-width:400px;width:100%;text-align:center}
h1{font-size:1.3rem;margin-bottom:8px}
.sub{color:#6868a0;font-size:0.85rem;margin-bottom:24px}
.spinner{width:32px;height:32px;border:3px solid #2a2a45;border-top-color:#c060ff;border-radius:50%;animation:spin .8s linear infinite;margin:0 auto 16px}
@keyframes spin{to{transform:rotate(360deg)}}
.msg{font-size:0.9rem;margin-bottom:24px;line-height:1.6}
.btn{display:block;width:100%;padding:14px;border-radius:10px;border:none;font-size:1rem;font-weight:700;cursor:pointer;text-decoration:none;margin-bottom:10px;text-align:center}
.btn-main{background:linear-gradient(135deg,#5020b0,#c060ff);color:#fff}
.btn-store{background:#20203a;color:#dddaf8;border:1px solid #2a2a45}
.btn-store:hover{background:#2a2a45}
.hidden{display:none}
.sep{color:#6868a0;font-size:0.8rem;margin:12px 0}
</style>
</head>
<body>
<div class="card">
<h1>Minuteur Dragodinde</h1>
<p class="sub">Notifications mobiles</p>
<div id="loading">
<div class="spinner"></div>
<p class="msg">Ouverture de l'app ntfy...</p>
</div>
<div id="fallback" class="hidden">
<p class="msg">Si l'app ntfy ne s'est pas ouverte, installe-la d'abord :</p>
<a id="btn-play" class="btn btn-store" href="#" target="_blank">Android — Play Store</a>
<a id="btn-apple" class="btn btn-store" href="#" target="_blank">iOS — App Store</a>
<div class="sep">puis reviens sur cette page</div>
<button id="btn-retry" class="btn btn-main" onclick="doRedirect()">Ouvrir dans ntfy</button>
</div>
</div>
<script>
const params = new URLSearchParams(window.location.search);
const topic = params.get('t');
const server = params.get('s') || 'ntfy.mickael-pol.fr';
const name = params.get('n') || 'dd-timer';
const ntfyLink = 'ntfy://' + server + '/' + topic + '?display=' + encodeURIComponent(name);
const playStore = 'https://play.google.com/store/apps/details?id=io.heckel.ntfy';
const appStore = 'https://apps.apple.com/app/ntfy/id1625396347';
document.getElementById('btn-play').href = playStore;
document.getElementById('btn-apple').href = appStore;
function doRedirect() {
if (!topic) return;
window.location = ntfyLink;
}
// Tenter la redirection automatique
if (topic) {
doRedirect();
// Afficher le fallback après 2s si on est toujours sur la page
setTimeout(function() {
document.getElementById('loading').classList.add('hidden');
document.getElementById('fallback').classList.remove('hidden');
}, 2000);
} else {
document.getElementById('loading').innerHTML = '<p class="msg" style="color:#ff5070">Lien invalide — aucun topic fourni.</p>';
}
</script>
</body>
</html>

View File

@ -1,6 +1,6 @@
{ {
"name": "minuteur-dragodinde", "name": "minuteur-dragodinde",
"version": "1.1.2", "version": "1.0.0",
"description": "Minuteur elevage Dragodinde Dofus 3", "description": "Minuteur elevage Dragodinde Dofus 3",
"main": "main.js", "main": "main.js",
"author": "Mickael", "author": "Mickael",
@ -9,7 +9,7 @@
"build": "electron-builder --win --x64" "build": "electron-builder --win --x64"
}, },
"build": { "build": {
"appId": "fr.mickael-pol.minuteur-dragodinde", "appId": "com.dofus3.minuteur-dragodinde",
"productName": "Minuteur Dragodinde", "productName": "Minuteur Dragodinde",
"directories": { "directories": {
"output": "dist" "output": "dist"
@ -41,8 +41,7 @@
"createDesktopShortcut": true, "createDesktopShortcut": true,
"createStartMenuShortcut": true, "createStartMenuShortcut": true,
"shortcutName": "Minuteur Dragodinde", "shortcutName": "Minuteur Dragodinde",
"deleteAppDataOnUninstall": false, "deleteAppDataOnUninstall": false
"runAfterFinish": true
} }
}, },
"devDependencies": { "devDependencies": {
@ -52,6 +51,5 @@
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://gitea.mickael-pol.fr/mickael/dd-timer.git" "url": "https://gitea.mickael-pol.fr/mickael/dd-timer.git"
}, }
"productName": "Minuteur Dragodinde"
} }

View File

@ -3,14 +3,9 @@ const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
isElectron: true, isElectron: true,
// Sauvegarde persistante (fichier JSON dans userData)
saveData: (json) => ipcRenderer.send('save-data', json),
loadData: () => ipcRenderer.invoke('load-data'),
// Alarme // Alarme
triggerAlarm: (enclosName) => ipcRenderer.send('trigger-alarm', { enclosName }), triggerAlarm: (enclosName) => ipcRenderer.send('trigger-alarm', { enclosName }),
showNotification: (title, body) => ipcRenderer.send('show-notification', { title, body }), showNotification: (title, body) => ipcRenderer.send('show-notification', { title, body }),
sendNtfy: (url, title, message) => ipcRenderer.send('send-ntfy', { url, title, message }),
focusWindow: () => ipcRenderer.send('focus-window'), focusWindow: () => ipcRenderer.send('focus-window'),
onPlayAlarmSound: (cb) => ipcRenderer.on('play-alarm-sound', () => cb()), onPlayAlarmSound: (cb) => ipcRenderer.on('play-alarm-sound', () => cb()),

File diff suppressed because one or more lines are too long