Compare commits

..

2 Commits

5 changed files with 100 additions and 23 deletions

View File

@ -70,6 +70,15 @@ dd-timer/
## Changelog
### v1.1.3
- ✨ **Reinitialisation des statistiques** — bouton dans l'onglet Statistiques pour remettre a zero tous les bebes, historiques et stats archivees (avec confirmation)
- ✨ **Inputs intelligents** — les champs numeriques se vident au clic pour saisir facilement, et restaurent la valeur precedente si rien n'est change
- 🔧 Correction "vider l'enclos" / "nouvelle fournee" / "supprimer enclos" — remplacement de confirm() par un dialogue natif Electron pour corriger le bug qui rendait tous les inputs inutilisables
- 🔧 Correction du bug "enclos vide" — apres avoir vide un enclos ou lance une nouvelle fournee, une dragodinde est automatiquement ajoutee et les inputs restent editables
- 🔧 Correction "nouvelle fournee" — ne vide plus l'historique de session des bebes, seul "vider l'enclos" le fait
- 🔧 Correction des statistiques globales — supprimer un enclos conserve desormais ses stats dans les statistiques globales (archivage automatique)
- 🔧 Correction du bouton "Quitter" — utilisation de process.exit() pour forcer l'arret complet du processus
### v1.1.2
- 💾 **Sauvegarde persistante** — les donnees sont maintenant sauvegardees dans un fichier JSON, plus de perte de statistiques apres une mise a jour
- ❌ **Dialogue de fermeture** — cliquer sur la croix propose de minimiser en arriere-plan ou de quitter completement

18
main.js
View File

@ -65,8 +65,8 @@ function createWindow() {
detail: 'Minimiser garde l\'app en arriere-plan.\nLes alarmes continueront de sonner.',
});
if (choice === 1) {
isQuitting = true;
app.quit();
if (tray) { tray.destroy(); tray = null; }
process.exit(0);
} else {
mainWindow.hide();
}
@ -142,6 +142,20 @@ ipcMain.on('show-notification', (event, { title, body }) => {
fireNotification(title, body);
});
// Dialogue de confirmation natif (remplace confirm() du renderer qui casse les inputs)
ipcMain.handle('show-confirm', (event, { title, message, detail }) => {
const choice = dialog.showMessageBoxSync(mainWindow, {
type: 'question',
buttons: ['Annuler', 'Confirmer'],
defaultId: 0,
cancelId: 0,
title: title || 'Confirmation',
message: message || '',
detail: detail || '',
});
return choice === 1;
});
// ─── SAUVEGARDE FICHIER (persistante entre mises à jour) ─────────────────
const dataFile = path.join(app.getPath('userData'), 'dd-timer-data.json');

View File

@ -1,6 +1,6 @@
{
"name": "minuteur-dragodinde",
"version": "1.1.2",
"version": "1.1.3",
"description": "Minuteur elevage Dragodinde Dofus 3",
"main": "main.js",
"author": "Mickael",

View File

@ -12,6 +12,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
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'),
showConfirm: (title, message, detail) => ipcRenderer.invoke('show-confirm', { title, message, detail }),
onPlayAlarmSound: (cb) => ipcRenderer.on('play-alarm-sound', () => cb()),
// Version

View File

@ -569,7 +569,7 @@ const RACES_DATA={
};
let S={enclos:[],activeId:null,nextEnclosId:1,alarmSound:'arpege',notifsEnabled:true,ntfyTopic:''};
let S={enclos:[],activeId:null,nextEnclosId:1,alarmSound:'arpege',notifsEnabled:true,ntfyTopic:'',archivedStats:[]};
const NTFY_BASE='https://ntfy.mickael-pol.fr';
const NTFY_REDIRECT='https://ntfy-redirect.mickael-pol.fr';
const lastTickIdx={};
@ -856,11 +856,22 @@ function addEnclos(){
S.enclos.push(e);S.activeId=e.id;
save();render();addDD(e.id);
}
function removeEnclos(id){
async function removeEnclos(id){
if(S.enclos.length<=1)return;
const enc=S.enclos.find(e=>e.id===id);
if(!enc)return;
const ok=await window.electronAPI.showConfirm('Supprimer l\'enclos',`Supprimer "${enc.name}" ?`,'Les statistiques globales seront conservees.');
if(!ok)return;
// Archiver les stats (babyHistory) pour les conserver dans les statistiques globales
const hist=enc.babyHistory||enc.babies||[];
if(hist.length){
if(!S.archivedStats)S.archivedStats=[];
S.archivedStats.push(...hist.map(b=>({...b,fromEnclos:enc.name})));
}
S.enclos=S.enclos.filter(e=>e.id!==id);
if(S.activeId===id||S.activeId==='dashboard')S.activeId=S.enclos[0].id;
save();render();
save();
render();
}
function selectEnclos(id){S.activeId=id;save();renderTabs();renderContent();}
@ -915,20 +926,23 @@ function removeDD(encId,ddId){
enc.dragodindes=enc.dragodindes.filter(d=>d.id!==ddId);
save();renderDDs(enc);
}
function viderEnclos(encId){
async function viderEnclos(encId){
const enc=S.enclos.find(e=>e.id===encId);
if(!enc)return;
if(!confirm(`Vider "${enc.name}" ?\n\nToutes les dragodindes seront supprimées.\nLes bébés et statistiques sont conservés.`))return;
const ok=await window.electronAPI.showConfirm('Vider l\'enclos',`Vider "${enc.name}" ?`,'Toutes les dragodindes seront supprimees.\nLes bebes et statistiques sont conserves.');
if(!ok)return;
enc.dragodindes=[];
enc.nextDdId=1;
enc.timer={running:false,startTime:null,pausedAt:null,pausedMs:0,snapGauges:{},snapStats:{}};
enc.alerted={};
enc.activeGauges=[];
Object.keys(enc.gaugeLevels).forEach(k=>{enc.gaugeLevels[k]=0;});
// babies session vide mais babyHistory intact
enc.babies=[];
lockInputs(encId,false);
save();renderContent();
// Ajouter une DD par défaut
const newId=enc.nextDdId++;
enc.dragodindes.push({id:newId,name:`Dragodinde ${newId}`,stats:{serenite:0,endurance:0,maturite:0,amour:0,xp:1},targets:{...DEFAULT_TARGETS}});
save();
render();
}
function updateName(encId,ddId,v){
const enc=S.enclos.find(e=>e.id===encId);
@ -956,18 +970,22 @@ function updateStatDirect(encId,ddId,stat,v){
save();updateLive();
}
function nouvelleFournee(encId){
async function nouvelleFournee(encId){
const enc=S.enclos.find(e=>e.id===encId);
if(!enc)return;
if(!confirm(`Nouvelle fournée pour ${enc.name} ?\n\nLes dragodindes actuelles seront supprimées.\nLes bébés et l'historique sont conservés.`))return;
const ok=await window.electronAPI.showConfirm('Nouvelle fournee',`Nouvelle fournee pour ${enc.name} ?`,'Les dragodindes actuelles seront supprimees.\nLes bebes et l\'historique sont conserves.');
if(!ok)return;
enc.dragodindes=[];
enc.nextDdId=1;
enc.timer={running:false,startTime:null,pausedAt:null,pausedMs:0,snapGauges:{},snapStats:{}};
enc.alerted={};
enc.activeGauges=[];
Object.keys(enc.gaugeLevels).forEach(k=>{enc.gaugeLevels[k]=0;});
enc.babies=[];
save();renderContent();
// Ajouter une DD par défaut
const newId=enc.nextDdId++;
enc.dragodindes.push({id:newId,name:`Dragodinde ${newId}`,stats:{serenite:0,endurance:0,maturite:0,amour:0,xp:1},targets:{...DEFAULT_TARGETS}});
save();
render();
}
function updateStat(encId,ddId,gid,v){
const enc=S.enclos.find(e=>e.id===encId);
@ -1070,6 +1088,7 @@ async function load(){
S.alarmSound=d.alarmSound||'arpege';
S.notifsEnabled=d.notifsEnabled!==undefined?d.notifsEnabled:true;
S.ntfyTopic=d.ntfyTopic||'';
S.archivedStats=d.archivedStats||[];
// Migration: ancien format ntfyUrl → ntfyTopic
if(!S.ntfyTopic&&d.ntfyUrl){const m=d.ntfyUrl.match(/\/([^\/]+)$/);if(m)S.ntfyTopic=m[1];}
S.enclos.forEach(enc=>{
@ -1315,6 +1334,8 @@ function renderGaugesCfg(enc){
<input type="number" class="gauge-inp" min="0" max="100000" step="1000"
value="${lvl}" style="border-color:${color}44"
oninput="updateGaugeLevel(${enc.id},'${gid}',this.value)"
onfocus="this.dataset.prev=this.value;this.value=''"
onblur="if(this.value==='')this.value=this.dataset.prev"
${enc.timer.running?'disabled':''}>
<span style="color:var(--muted);font-size:11px">/ 100 000</span>
</div>
@ -1390,6 +1411,8 @@ function renderDDs(enc){
<input type="number" id="pstat-${enc.id}-${dd.id}-${p.stat}"
value="${val}" min="${p.min}" max="${p.max}" step="${p.stat==='xp'?1:10}"
oninput="updateStatDirect(${enc.id},${dd.id},'${p.stat}',this.value)"
onfocus="this.dataset.prev=this.value;this.value=''"
onblur="if(this.value==='')this.value=this.dataset.prev"
title="${p.stat}">
</div>`;
});
@ -1987,17 +2010,41 @@ function changeCount(delta){
// ══════════════════════════════════════════
// STATS TAB
// ══════════════════════════════════════════
async function resetStats(){
const ok=await window.electronAPI.showConfirm(
'Reinitialiser les statistiques',
'Reinitialiser toutes les statistiques ?',
'Cette action est irreversible.\nTous les bebes enregistres, l\'historique de chaque enclos et les statistiques archivees seront definitivement supprimes.'
);
if(!ok)return;
S.archivedStats=[];
S.enclos.forEach(enc=>{
enc.babies=[];
enc.babyHistory=[];
});
save();
render();
}
function renderStats(){
const c=document.getElementById('enclos-content');
c.removeAttribute('data-enc');
// Agréger toutes les données
// Agréger toutes les données (enclos actifs + stats archivées des enclos supprimés)
let totalDD=0,totalBabies=0,totalSessions=0;
const raceMap={};
const enclosStats=[];
// Stats archivées (enclos supprimés)
const archived=S.archivedStats||[];
archived.forEach(b=>{
if(!raceMap[b.race])raceMap[b.race]={count:0,gen:b.gen};
raceMap[b.race].count+=b.count;
totalBabies+=b.count;
});
const archivedMax=archived.reduce((s,b)=>s+Math.min(5,Math.floor((b.ddCount||1)/2)),0);
S.enclos.forEach(enc=>{
const babies=(enc.babyHistory||enc.babies||[]); // utilise l'historique permanent
const babies=(enc.babyHistory||enc.babies||[]);
const nbBabies=babies.reduce((s,b)=>s+b.count,0);
const nbDD=enc.dragodindes.length;
totalDD+=nbDD;
@ -2011,8 +2058,6 @@ function renderStats(){
});
if(nbBabies>0){
// Calcul taux de réussite basé sur le ddCount stocké lors de l'ajout
// → résiste à la suppression/nouvelle fournée de DD
const maxPossible=babies.reduce((s,b)=>{
const m=Math.min(5,Math.floor((b.ddCount||1)/2));
return s+m;
@ -2022,8 +2067,8 @@ function renderStats(){
}
});
// Taux global basé sur les ddCount stockés
const totalMax=S.enclos.reduce((s,e)=>{
// Taux global (enclos actifs + archivés)
const totalMax=archivedMax+S.enclos.reduce((s,e)=>{
const hist=(e.babyHistory||e.babies||[]);
return s+hist.reduce((ss,b)=>ss+Math.min(5,Math.floor((b.ddCount||1)/2)),0);
},0);
@ -2034,8 +2079,16 @@ function renderStats(){
const maxRaceCount=racesSorted[0]?.[1].count||1;
c.innerHTML=`
<div style="font-family:'Cinzel',serif;font-size:0.8rem;letter-spacing:2.5px;text-transform:uppercase;color:var(--muted);margin-bottom:6px">
Statistiques globales
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
<div style="font-family:'Cinzel',serif;font-size:0.8rem;letter-spacing:2.5px;text-transform:uppercase;color:var(--muted)">
Statistiques globales
</div>
<button onclick="resetStats()" style="padding:6px 14px;border-radius:8px;border:1px solid var(--border);background:transparent;color:var(--muted);cursor:pointer;font:700 0.82rem 'Nunito',sans-serif;transition:.15s"
onmouseover="this.style.borderColor='var(--amour)';this.style.color='var(--amour)'"
onmouseout="this.style.borderColor='var(--border)';this.style.color='var(--muted)'"
title="Remettre toutes les statistiques a zero">
🗑 Reinitialiser les statistiques
</button>
</div>
<div class="stats-wrap">
<div class="stats-section">