Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3770d5d06 | |||
| 17a0b1c7da |
55
CLAUDE.md
Normal file
55
CLAUDE.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# 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`
|
||||||
77
README.md
77
README.md
@ -11,10 +11,11 @@ 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
|
||||||
- 🔊 **5 sons d'alarme** au choix (Arpège, Pulsation, Fanfare, Sirène, Cloche)
|
- 🔊 **4 sons d'alarme** au choix (Arpège, Pulsation, Fanfare, 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)
|
||||||
@ -69,63 +70,23 @@ dd-timer/
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
### v1.0.0
|
### v1.1.0
|
||||||
- Version initiale
|
- 📱 **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)
|
||||||
Outil de gestion d'élevage de Dragodindes pour Dofus 3.
|
- QR code d'abonnement automatique via page de redirection (ntfy-redirect)
|
||||||
Application desktop Windows construite avec Electron.
|
- Bouton de test des notifications
|
||||||
|
- Envoi via le processus principal Electron (pas de CORS)
|
||||||
## Fonctionnalités
|
- 🐣 **Systeme de bebes** — Ajout de bebes dragodindes issus de la reproduction dans chaque enclos
|
||||||
|
- Modale de selection par generation et race avec images
|
||||||
- 🐦 Gestion de **6 enclos indépendants** avec jusqu'à 10 Dragodindes chacun
|
- Historique des bebes par enclos
|
||||||
- ⏱ **Timer en temps réel** avec calcul automatique par tier de jauge (1→4)
|
- 📊 **Onglet Statistiques** — Vue globale de l'elevage avec KPIs, repartition par race, et progression par enclos
|
||||||
- 📊 **Dashboard** vue d'ensemble multi-enclos
|
- 🖱 **Drag & drop** des onglets d'enclos pour les reordonner
|
||||||
- 🔔 **Notifications natives Windows** même application en arrière-plan
|
- 🐉 **Images des dragodindes** par race avec couleurs par generation
|
||||||
- 🔊 **5 sons d'alarme** au choix (Arpège, Pulsation, Fanfare, Sirène, Cloche)
|
- 📝 **Sous-onglets par enclos** (Elevage / Historique bebes)
|
||||||
- 🐉 **Jauges** : Baffeur, Caresseur, Foudroyeur, Abreuvoir, Dragofesse, Mangeoire (XP)
|
- 🔧 **Mode DEV** — Donnees isolees et badge DEV visible quand lance avec `npm start`
|
||||||
- 🖱 **Drag & drop** des enclos et des Dragodindes pour les réordonner
|
- ⬆ **Mise a jour automatique** via Gitea Releases avec banniere de progression dans l'interface
|
||||||
- 💾 Sauvegarde automatique locale
|
- 🔧 Correction de l'identifiant applicatif (`fr.mickael-pol.minuteur-dragodinde`)
|
||||||
|
- 🔧 Masquage des spinners natifs sur les champs numeriques
|
||||||
## Installation (utilisateurs)
|
|
||||||
|
|
||||||
1. Télécharger `Minuteur Dragodinde Setup x.x.x.exe` depuis la section [Releases](../../releases)
|
|
||||||
2. **Clic droit → Propriétés → cocher "Débloquer" → OK** (important, une seule fois)
|
|
||||||
3. Double-cliquer pour lancer l'installation → suivre l'assistant
|
|
||||||
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
|
||||||
|
|||||||
58
main.js
58
main.js
@ -4,6 +4,19 @@ 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
|
||||||
@ -57,6 +70,19 @@ 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(()=>{});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +133,38 @@ ipcMain.on('show-notification', (event, { title, body }) => {
|
|||||||
fireNotification(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(); });
|
ipcMain.on('focus-window', () => { mainWindow.show(); mainWindow.focus(); });
|
||||||
|
|
||||||
// Renderer demande à installer la mise à jour
|
// Renderer demande à installer la mise à jour
|
||||||
|
|||||||
74
ntfy-redirect/index.html
Normal file
74
ntfy-redirect/index.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<!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>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "minuteur-dragodinde",
|
"name": "minuteur-dragodinde",
|
||||||
"version": "1.0.0",
|
"version": "1.1.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": "com.dofus3.minuteur-dragodinde",
|
"appId": "fr.mickael-pol.minuteur-dragodinde",
|
||||||
"productName": "Minuteur Dragodinde",
|
"productName": "Minuteur Dragodinde",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "dist"
|
"output": "dist"
|
||||||
@ -51,5 +51,6 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@ -6,6 +6,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
// 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()),
|
||||||
|
|
||||||
|
|||||||
1101
src/index.html
1101
src/index.html
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user