import type { CommandBus } from '@application/handlers/CommandBus'; import type { QueryBus } from '@application/handlers/QueryBus'; import type { SettingsResult } from '@application/queries/GetSettings'; import { esc } from '@presentation/helpers/format'; import { Toast } from './Toast'; import { ConfirmModal } from './ConfirmModal'; const NTFY_BASE = 'https://ntfy.mickael-pol.fr'; const NTFY_REDIRECT = 'https://ntfy-redirect.mickael-pol.fr'; const SOUND_OPTIONS: { value: string; label: string; icon: string }[] = [ { value: 'arpege', label: 'Arpège', icon: 'music_note' }, { value: 'pulse', label: 'Pulsation', icon: 'pulse_alert' }, { value: 'fanfare', label: 'Fanfare', icon: 'celebration' }, { value: 'cloche', label: 'Cloche', icon: 'notifications_active' }, ]; export class ParametresView { private el: HTMLElement | null = null; private modal: HTMLElement | null = null; constructor( private commandBus: CommandBus, private queryBus: QueryBus, private playSound?: (name: string) => void, ) {} render(container: HTMLElement): void { this.el = document.createElement('div'); this.el.className = 'param-view'; container.appendChild(this.el); this.updateDOM(); } update(): void {} destroy(): void { this.modal?.remove(); this.modal = null; this.el?.remove(); this.el = null; } private getSettings(): SettingsResult { return this.queryBus.execute({ type: 'get-settings' }); } private updateDOM(): void { if (!this.el) return; const { alarmSound, notifsEnabled, ntfyTopic } = this.getSettings(); let html = ''; // ── Hero ────────────────────────────────────────────────────── html += `

Param\u00e8tres

Configuration de l'application et des notifications.

`; // ── Son d'alarme ────────────────────────────────────────────── html += `
volume_up

Son d'alarme

Choisissez le son joué lorsqu'un enclos termine sa session.

`; for (const opt of SOUND_OPTIONS) { const isActive = alarmSound === opt.value ? ' active' : ''; html += ``; } html += `
`; // ── Notifications PC ────────────────────────────────────────── html += `
notifications

Notifications PC

Recevez une notification Windows quand un enclos est pr\u00eat.

${notifsEnabled ? 'Notifications activ\u00e9es' : 'Notifications d\u00e9sactiv\u00e9es'}
`; // ── Notifications Mobile ────────────────────────────────────── const mobileActive = ntfyTopic ? ' active' : ''; html += `
phone_android

Notifications Mobiles

Recevez une alerte sur votre t\u00e9l\u00e9phone via ntfy, m\u00eame loin de votre PC.

${ntfyTopic ? 'Connect\u00e9 — notifications actives' : 'Non configur\u00e9'}
`; // ── Données ────────────────────────────────────────────────── html += `
database

Donn\u00e9es

Exportez ou importez toutes vos donn\u00e9es (enclos, dragodindes, statistiques, workflows).

`; this.el.innerHTML = html; this.bindEvents(); } private bindEvents(): void { if (!this.el) return; // Sound cards this.el.querySelectorAll('.param-sound-card').forEach(card => { card.addEventListener('click', () => { const sound = card.dataset['sound']!; this.commandBus.execute({ type: 'update-settings', alarmSound: sound }); this.updateDOM(); }); }); // Test sound this.el.querySelector('#param-test-sound')?.addEventListener('click', () => { const { alarmSound } = this.getSettings(); this.playSound?.(alarmSound); }); // PC notifications toggle this.el.querySelector('#param-notifs-toggle')?.addEventListener('click', () => { const { notifsEnabled } = this.getSettings(); this.commandBus.execute({ type: 'update-settings', notifsEnabled: !notifsEnabled }); this.updateDOM(); }); // Ntfy modal this.el.querySelector('#param-ntfy-btn')?.addEventListener('click', () => { this.openNtfyModal(); }); // Export data this.el.querySelector('#param-export-data')?.addEventListener('click', () => this.exportData()); // Import data this.el.querySelector('#param-import-data')?.addEventListener('click', () => this.importData()); } /* ══ Modal ntfy ══ */ private openNtfyModal(): void { if (this.modal) { this.modal.classList.remove('hidden'); this.renderNtfyModal(); return; } this.modal = document.createElement('div'); this.modal.className = 'param-modal-overlay'; this.modal.innerHTML = `
phone_android

Notifications mobiles

`; document.body.appendChild(this.modal); this.modal.querySelector('#ntfy-modal-close')?.addEventListener('click', () => this.closeNtfyModal()); this.modal.querySelector('#ntfy-modal-footer-close')?.addEventListener('click', () => this.closeNtfyModal()); this.modal.addEventListener('click', (e) => { if (e.target === this.modal) this.closeNtfyModal(); }); this.renderNtfyModal(); } private closeNtfyModal(): void { this.modal?.classList.add('hidden'); } private renderNtfyModal(): void { const body = this.modal?.querySelector('#ntfy-modal-body'); if (!body) return; const { ntfyTopic } = this.getSettings(); if (!ntfyTopic) { body.innerHTML = `
notifications_active

Recevez une alerte sur votre t\u00e9l\u00e9phone quand un enclos est pr\u00eat, m\u00eame si votre PC est loin !

`; body.querySelector('#ntfy-activate')?.addEventListener('click', () => this.activateNtfy()); return; } // QR codes const ntfyPlayStore = 'https://play.google.com/store/apps/details?id=io.heckel.ntfy'; const ntfyAppStore = 'https://apps.apple.com/app/ntfy/id1625396347'; const qrDownload = `https://api.qrserver.com/v1/create-qr-code/?size=160x160&data=${encodeURIComponent(ntfyPlayStore)}`; const redirectUrl = `${NTFY_REDIRECT}/?t=${encodeURIComponent(ntfyTopic)}&s=${encodeURIComponent(NTFY_BASE.replace(/^https?:\/\//, ''))}&n=dd-timer`; const qrSubscribe = `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(redirectUrl)}`; body.innerHTML = `
1

Installer l'app ntfy

Scannez ce QR code ou cherchez ntfy sur le Play Store / App Store

T\u00e9l\u00e9charger ntfy
2

S'abonner aux notifications

Scannez ce QR code avec l'appareil photo de votre t\u00e9l\u00e9phone pour ajouter automatiquement les notifications.

S'abonner aux notifications
`; body.querySelector('#ntfy-test')?.addEventListener('click', () => this.testNtfy()); body.querySelector('#ntfy-deactivate')?.addEventListener('click', () => this.deactivateNtfy()); } private activateNtfy(): void { const topic = 'dd-' + Math.random().toString(36).slice(2, 8) + '-' + Date.now().toString(36).slice(-4); this.commandBus.execute({ type: 'update-settings', ntfyTopic: topic }); this.updateDOM(); this.renderNtfyModal(); } private deactivateNtfy(): void { this.commandBus.execute({ type: 'update-settings', ntfyTopic: '' }); this.updateDOM(); this.renderNtfyModal(); } private testNtfy(): void { const { ntfyTopic } = this.getSettings(); if (!ntfyTopic) return; const url = `${NTFY_BASE}/${ntfyTopic}`; (window as any).electronAPI?.sendNtfy?.(url, 'Test alarme', 'Ceci est un test de la notification mobile Minuteur Dragodinde !'); } /* ══ Backup / Restore ══ */ private async exportData(): Promise { const api = (window as any).electronAPI; if (!api?.loadData || !api?.exportFile) return; try { const raw = await api.loadData(); if (!raw) { Toast.show('error', 'Aucune donnée à exporter.'); return; } const version = await api.getVersion?.() ?? 'unknown'; const backup = { app: 'minuteur-dragodinde', version, exportedAt: new Date().toISOString(), data: JSON.parse(raw), }; const date = new Date().toISOString().slice(0, 10); const ok = await api.exportFile(JSON.stringify(backup, null, 2), `dd-timer-backup-${date}.json`); if (ok) { Toast.show('success', 'Données exportées avec succès.'); } } catch { Toast.show('error', 'Erreur lors de l\'export des données.'); } } private async importData(): Promise { const api = (window as any).electronAPI; if (!api?.importFile || !api?.saveData) return; try { const raw = await api.importFile(); if (!raw) return; // dialogue annulé let parsed: any; try { parsed = JSON.parse(raw); } catch { Toast.show('error', 'Le fichier sélectionné n\'est pas un JSON valide.'); return; } // Validation structurelle des données importées const validateEnclosData = (data: any): boolean => { if (!data || typeof data !== 'object' || data === null) return false; if (!Array.isArray(data.enclos)) return false; return data.enclos.every((enc: any) => enc && typeof enc === 'object' && typeof enc.id === 'number' && typeof enc.name === 'string' && Array.isArray(enc.dragodindes) && enc.gaugeLevels && typeof enc.gaugeLevels === 'object' && enc.timer && typeof enc.timer === 'object', ); }; // Validation du format backup if (parsed.app === 'minuteur-dragodinde' && parsed.data && typeof parsed.data === 'object' && parsed.data !== null) { if (!validateEnclosData(parsed.data)) { Toast.show('error', 'Le backup contient des données corrompues ou incomplètes.'); return; } const date = parsed.exportedAt ? new Date(parsed.exportedAt).toLocaleDateString('fr-FR') : 'inconnue'; const ok = await ConfirmModal.show( 'Importer les données', `Ce backup date du ${date} (v${parsed.version ?? '?'}). Toutes vos données actuelles seront remplacées. Continuer ?`, ); if (!ok) return; api.saveData(JSON.stringify(parsed.data)); } else if (parsed.enclos && Array.isArray(parsed.enclos)) { if (!validateEnclosData(parsed)) { Toast.show('error', 'Le fichier contient des données corrompues ou incomplètes.'); return; } const ok = await ConfirmModal.show( 'Importer les données', 'Toutes vos données actuelles seront remplacées. Continuer ?', ); if (!ok) return; api.saveData(JSON.stringify(parsed)); } else { Toast.show('error', 'Format de fichier non reconnu.'); return; } Toast.show('success', 'Données importées. Rechargement...'); setTimeout(() => window.location.reload(), 1000); } catch { Toast.show('error', 'Erreur lors de l\'import des données.'); } } }