| import { Panel } from './Panel'; |
| import { t } from '@/services/i18n'; |
| import { |
| COUNTER_METRICS, |
| getCounterValue, |
| formatCounterValue, |
| type CounterMetric, |
| } from '@/services/humanity-counters'; |
| import { isDesktopRuntime } from '@/services/runtime'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export class CountersPanel extends Panel { |
| private animFrameId: number | null = null; |
| private valueElements: Map<string, HTMLElement> = new Map(); |
| private readonly desktopMode = isDesktopRuntime(); |
| private visibilityHandler: (() => void) | null = null; |
| private lastDesktopUpdateAt = 0; |
| private readonly desktopUpdateIntervalMs = 250; |
|
|
| constructor() { |
| super({ id: 'counters', title: 'Live Counters', trackActivity: false, infoTooltip: t('components.counters.infoTooltip') }); |
| this.createCounterGrid(); |
| if (this.desktopMode) { |
| this.visibilityHandler = () => { |
| if (document.hidden) { |
| this.stopTicking(); |
| return; |
| } |
| this.lastDesktopUpdateAt = 0; |
| this.startTicking(); |
| }; |
| document.addEventListener('visibilitychange', this.visibilityHandler); |
| } |
| this.startTicking(); |
| } |
|
|
| |
| |
| |
| private createCounterGrid(): void { |
| const grid = document.createElement('div'); |
| grid.className = 'counters-grid'; |
|
|
| for (const metric of COUNTER_METRICS) { |
| const card = this.createCounterCard(metric); |
| grid.appendChild(card); |
| } |
|
|
| |
| this.content.innerHTML = ''; |
| this.content.appendChild(grid); |
| } |
|
|
| |
| |
| |
| private createCounterCard(metric: CounterMetric): HTMLElement { |
| const card = document.createElement('div'); |
| card.className = 'counter-card'; |
|
|
| const icon = document.createElement('div'); |
| icon.className = 'counter-icon'; |
| icon.textContent = metric.icon; |
|
|
| const value = document.createElement('div'); |
| value.className = 'counter-value'; |
| value.dataset.counter = metric.id; |
| |
| value.textContent = formatCounterValue( |
| getCounterValue(metric), |
| metric.formatPrecision, |
| ); |
|
|
| const label = document.createElement('div'); |
| label.className = 'counter-label'; |
| label.textContent = metric.label; |
|
|
| const source = document.createElement('div'); |
| source.className = 'counter-source'; |
| source.textContent = metric.source; |
|
|
| card.appendChild(icon); |
| card.appendChild(value); |
| card.appendChild(label); |
| card.appendChild(source); |
|
|
| |
| this.valueElements.set(metric.id, value); |
|
|
| return card; |
| } |
|
|
| |
| |
| |
| |
| public startTicking(): void { |
| if (this.animFrameId !== null) return; |
| if (this.desktopMode && document.hidden) return; |
| this.animFrameId = requestAnimationFrame(this.tick); |
| } |
|
|
| private stopTicking(): void { |
| if (this.animFrameId !== null) { |
| cancelAnimationFrame(this.animFrameId); |
| this.animFrameId = null; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| private tick = (): void => { |
| if (this.desktopMode) { |
| const now = performance.now(); |
| if ((now - this.lastDesktopUpdateAt) < this.desktopUpdateIntervalMs) { |
| this.animFrameId = requestAnimationFrame(this.tick); |
| return; |
| } |
| this.lastDesktopUpdateAt = now; |
| } |
|
|
| for (const metric of COUNTER_METRICS) { |
| const el = this.valueElements.get(metric.id); |
| if (el) { |
| const value = getCounterValue(metric); |
| el.textContent = formatCounterValue(value, metric.formatPrecision); |
| } |
| } |
| this.animFrameId = requestAnimationFrame(this.tick); |
| }; |
|
|
| |
| |
| |
| public destroy(): void { |
| this.stopTicking(); |
| if (this.visibilityHandler) { |
| document.removeEventListener('visibilitychange', this.visibilityHandler); |
| this.visibilityHandler = null; |
| } |
| this.valueElements.clear(); |
| super.destroy(); |
| } |
| } |
|
|