diff --git a/src/presentation/components/App.ts b/src/presentation/components/App.ts index dc84ec4..6ae3d3b 100755 --- a/src/presentation/components/App.ts +++ b/src/presentation/components/App.ts @@ -324,8 +324,18 @@ export class App { } // ── Live update loop ────────────────────────────────────────── + private lastRafTime = 0; + private startAnimationLoop(): void { - const loop = () => { + const loop = (now: number) => { + // Throttle à ~4fps quand aucun timer ne tourne (idle) + const data = this.getDashboardData(); + const anyRunning = data.enclosSummaries.some(e => e.running); + if (!anyRunning && now - this.lastRafTime < 250) { + this.rafId = requestAnimationFrame(loop); + return; + } + this.lastRafTime = now; this.updateTabDots(); if (this.activeChild) this.activeChild.update(); this.rafId = requestAnimationFrame(loop); diff --git a/src/presentation/components/EnclosView.ts b/src/presentation/components/EnclosView.ts index b4a38d5..d1e882c 100755 --- a/src/presentation/components/EnclosView.ts +++ b/src/presentation/components/EnclosView.ts @@ -15,10 +15,28 @@ import { UndoManager } from '@presentation/services/UndoManager'; const ALL_GAUGES: GaugeType[] = ['baffeur', 'caresseur', 'foudroyeur', 'abreuvoir', 'dragofesse', 'mangeoire']; +interface CachedGaugeEls { + tier: Element | null; + bar: HTMLElement | null; + empty: Element | null; + inp: HTMLInputElement | null; +} + export class EnclosView { private el: HTMLElement | null = null; private enclosId = 0; private ddCards: Map = new Map(); + private cachedEls: { + elapsed: Element | null; + gcd: Element | null; + tbtn: HTMLButtonElement | null; + resetBtn: HTMLElement | null; + doneBanner: HTMLElement | null; + doneResetBtn: HTMLElement | null; + ddCount: Element | null; + addBtn: HTMLButtonElement | null; + gauges: Map; + } | null = null; constructor( private commandBus: CommandBus, @@ -179,9 +197,35 @@ export class EnclosView { `; this.bindEvents(enc); + this.cacheElements(enc); this.renderDdCards(enc); } + private cacheElements(enc: Enclos): void { + if (!this.el) return; + const eId = this.enclosId; + const gauges = new Map(); + enc.activeGauges.forEach(gid => { + gauges.set(gid, { + tier: this.el!.querySelector(`#gtier-${eId}-${gid}`), + bar: this.el!.querySelector(`#gbar-${eId}-${gid}`), + empty: this.el!.querySelector(`#gempty-${eId}-${gid}`), + inp: this.el!.querySelector(`#glvl-${eId}-${gid}`), + }); + }); + this.cachedEls = { + elapsed: this.el.querySelector(`#elapsed-${eId}`), + gcd: this.el.querySelector(`#gcd-${eId}`), + tbtn: this.el.querySelector(`#tbtn-${eId}`), + resetBtn: this.el.querySelector(`#treset-${eId}`), + doneBanner: this.el.querySelector(`#done-banner-${eId}`), + doneResetBtn: this.el.querySelector(`#done-reset-${eId}`), + ddCount: this.el.querySelector(`#ddcount-${eId}`), + addBtn: this.el.querySelector(`#adddd-${eId}`), + gauges, + }; + } + private bindEvents(enc: Enclos): void { if (!this.el) return; const eId = this.enclosId; @@ -307,54 +351,52 @@ export class EnclosView { } update(): void { - if (!this.el) return; + if (!this.el || !this.cachedEls) return; const enc = this.getEnc(); - const eId = this.enclosId; - const { globalMax, allDone, started, el: elSec, ddDone } = enclosGlobalState(enc); + const c = this.cachedEls; + const { globalMax, allDone, started, el: elSec } = enclosGlobalState(enc); const running = enc.timer.running; /* Complétion automatique : toutes les cibles atteintes → une seule alarme */ if (allDone && running && enc.dragodindes.length > 0 && enc.activeGauges.length > 0) { - this.commandBus.execute({ type: 'complete-timer', enclosId: eId }); + this.commandBus.execute({ type: 'complete-timer', enclosId: this.enclosId }); this.renderInner(); return; } /* Elapsed clock */ - const elapsedEl = this.el.querySelector(`#elapsed-${eId}`); - if (elapsedEl) elapsedEl.textContent = fmtClock(elSec); + if (c.elapsed) c.elapsed.textContent = fmtClock(elSec); /* Global countdown */ - const gcdEl = this.el.querySelector(`#gcd-${eId}`); - if (gcdEl) { + if (c.gcd) { if (enc.activeGauges.length > 0 && enc.dragodindes.length > 0) { if (allDone) { - gcdEl.textContent = '✅'; + c.gcd.textContent = '✅'; } else if (!isFinite(globalMax)) { - gcdEl.textContent = '∞'; + c.gcd.textContent = '∞'; } else { - gcdEl.textContent = fmtClock(globalMax); + c.gcd.textContent = fmtClock(globalMax); } } else { - gcdEl.textContent = '--:--:--'; + c.gcd.textContent = '--:--:--'; } } /* Timer button state */ - const tbtn = this.el.querySelector(`#tbtn-${eId}`); - if (tbtn) { + if (c.tbtn) { const timerIcon = running ? 'pause' : 'play_arrow'; const timerText = running ? 'PAUSE' : (enc.timer.pausedAt && !enc.alerted['__done__'] ? 'REPRENDRE' : 'DÉMARRER'); - tbtn.className = running ? 'enc-start-btn enc-btn-pause' : 'enc-start-btn'; - tbtn.innerHTML = `${timerIcon}${timerText}`; + c.tbtn.className = running ? 'enc-start-btn enc-btn-pause' : 'enc-start-btn'; + c.tbtn.innerHTML = `${timerIcon}${timerText}`; } /* Reset button visibility */ - const resetBtn = this.el.querySelector(`#treset-${eId}`); - if (resetBtn) resetBtn.style.display = started ? '' : 'none'; + if (c.resetBtn) c.resetBtn.style.display = started ? '' : 'none'; - /* Gauge config live updates (tier badges and bars decay over time) */ + /* Gauge config live updates (cached — no querySelector per frame) */ enc.activeGauges.forEach(gid => { + const g = c.gauges.get(gid); + if (!g) return; const startGl = started ? (enc.timer.snapGauges[gid] ?? enc.gaugeLevels[gid]) : enc.gaugeLevels[gid]; const curGl = started ? enclosGaugeCurGl(enc, gid) : startGl; const tn = tierNum(curGl); @@ -363,62 +405,48 @@ export class EnclosView { const emptyTime = curGl > 0 ? timeToGain(curGl, curGl) : 0; const emptyStr = emptyTime === Infinity ? '∞' : fmt(emptyTime); - const tierEl = this.el!.querySelector(`#gtier-${eId}-${gid}`); - if (tierEl) { - tierEl.textContent = curGl > 0 ? `Tier ${tn} · ±${tr}/tick` : 'Jauge vide'; + if (g.tier) { + g.tier.textContent = curGl > 0 ? `Tier ${tn} · ±${tr}/tick` : 'Jauge vide'; } - - const barEl = this.el!.querySelector(`#gbar-${eId}-${gid}`); - if (barEl) barEl.style.width = `${pct.toFixed(1)}%`; - - const emptyEl = this.el!.querySelector(`#gempty-${eId}-${gid}`); - if (emptyEl) { + if (g.bar) g.bar.style.width = `${pct.toFixed(1)}%`; + if (g.empty) { if (curGl > 0) { - emptyEl.textContent = `Vide en ${emptyStr}`; - emptyEl.classList.remove('enc-gauge-alert'); + g.empty.textContent = `Vide en ${emptyStr}`; + g.empty.classList.remove('enc-gauge-alert'); } else if (started && running) { - emptyEl.textContent = '⚠ Rechargez la jauge'; - emptyEl.classList.add('enc-gauge-alert'); + g.empty.textContent = '⚠ Rechargez la jauge'; + g.empty.classList.add('enc-gauge-alert'); } else { - emptyEl.textContent = 'Vide'; - emptyEl.classList.remove('enc-gauge-alert'); + g.empty.textContent = 'Vide'; + g.empty.classList.remove('enc-gauge-alert'); } } - - /* Mettre à jour la valeur affichée de l'input et son état en temps réel */ - const inp = this.el!.querySelector(`#glvl-${eId}-${gid}`); - if (inp) { - // Synchroniser data-running avec l'état réel du timer - inp.dataset.running = started ? '1' : '0'; - // Mettre à jour la valeur affichée (sauf si l'utilisateur est en train de taper) - if (document.activeElement !== inp) { - inp.value = String(Math.round(started ? curGl : (enc.gaugeLevels[gid] ?? 0))); + if (g.inp) { + g.inp.dataset.running = started ? '1' : '0'; + if (document.activeElement !== g.inp) { + g.inp.value = String(Math.round(started ? curGl : (enc.gaugeLevels[gid] ?? 0))); } } }); /* Done banner */ - const doneBanner = this.el.querySelector(`#done-banner-${eId}`); - if (doneBanner) { - doneBanner.style.display = (allDone && started && enc.dragodindes.length > 0 && enc.activeGauges.length > 0) ? '' : 'none'; + if (c.doneBanner) { + c.doneBanner.style.display = (allDone && started && enc.dragodindes.length > 0 && enc.activeGauges.length > 0) ? '' : 'none'; } - /* Bouton "Nouvelle fournée" — visible uniquement si toutes les DD ont 20000 en maturité, endurance et amour */ - const doneResetBtn = this.el.querySelector(`#done-reset-${eId}`); - if (doneResetBtn) { + /* Bouton "Nouvelle fournée" */ + if (c.doneResetBtn) { const allMaxed = enc.dragodindes.length > 0 && enc.dragodindes.every(dd => dd.stats.maturite >= 20000 && dd.stats.endurance >= 20000 && dd.stats.amour >= 20000 ); - doneResetBtn.style.display = allMaxed ? '' : 'none'; + c.doneResetBtn.style.display = allMaxed ? '' : 'none'; } /* DD count */ - const ddCountEl = this.el.querySelector(`#ddcount-${eId}`); - if (ddCountEl) ddCountEl.textContent = `${enc.dragodindes.length}/10`; + if (c.ddCount) c.ddCount.textContent = `${enc.dragodindes.length}/10`; /* Add button state */ - const addBtn = this.el.querySelector(`#adddd-${eId}`); - if (addBtn) addBtn.disabled = enc.dragodindes.length >= 10; + if (c.addBtn) c.addBtn.disabled = enc.dragodindes.length >= 10; /* Update each DD card */ enc.dragodindes.forEach(dd => { @@ -439,6 +467,7 @@ export class EnclosView { destroy(): void { this.ddCards.forEach(card => card.destroy()); this.ddCards.clear(); + this.cachedEls = null; this.el?.remove(); this.el = null; } diff --git a/src/presentation/components/Sidebar.ts b/src/presentation/components/Sidebar.ts index 3136c73..4187d05 100755 --- a/src/presentation/components/Sidebar.ts +++ b/src/presentation/components/Sidebar.ts @@ -5,6 +5,7 @@ import { esc } from '@presentation/helpers/format'; export class Sidebar { private el: HTMLElement | null = null; + private cachedVersion: string | null = null; constructor( private uiState: UIState, @@ -32,7 +33,7 @@ export class Sidebar { html += `
- logo + logo
Obsidienne @@ -111,9 +112,15 @@ export class Sidebar { } private fetchAndInjectVersion(): void { + if (this.cachedVersion) { + const verEl = this.el?.querySelector('#sb-ver'); + if (verEl) verEl.textContent = `v${this.cachedVersion}`; + return; + } const api = (window as any).electronAPI; if (!api?.getVersion) return; api.getVersion().then((v: string) => { + this.cachedVersion = v; const verEl = this.el?.querySelector('#sb-ver'); if (verEl) verEl.textContent = `v${v}`; }).catch(() => {}); diff --git a/src/public/icone_sidebar.png b/src/public/icone_sidebar.png index 2f0f8a8..628ea03 100755 Binary files a/src/public/icone_sidebar.png and b/src/public/icone_sidebar.png differ