canvasframework 0.3.23 → 0.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,611 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Lecteur audio avec gestion directe des événements
5
+ * @class
6
+ * @extends Component
7
+ */
8
+ class AudioPlayer extends Component {
9
+ constructor(framework, options = {}) {
10
+ super(framework, options);
11
+ this.src = options.src || '';
12
+ this.playing = false;
13
+ this.platform = framework.platform;
14
+ this.showControls = true;
15
+ this.controlsTimeout = null;
16
+ this.currentTime = 0;
17
+ this.duration = 0;
18
+ this.progress = 0;
19
+ this.volume = 1;
20
+ this.showVolume = false;
21
+ this.loaded = false;
22
+ this.isLoading = false;
23
+ this.userInteracted = false;
24
+
25
+ // Élément audio HTML caché
26
+ this.audioElement = null;
27
+
28
+ // Gestion directe des événements
29
+ this.canvas = framework.canvas;
30
+ this.isMouseDownOnPlayer = false;
31
+ this.clickStartTime = 0;
32
+ this.clickThreshold = 200; // ms
33
+
34
+ this.controlsHeight = 50;
35
+
36
+ // Callbacks
37
+ this.onPlay = options.onPlay;
38
+ this.onPause = options.onPause;
39
+ this.onEnded = options.onEnded;
40
+
41
+ // NE PAS utiliser les handlers du framework
42
+ this.onPress = null;
43
+ this.onMove = null;
44
+ this.onRelease = null;
45
+
46
+ // Attacher les événements directement au canvas
47
+ this.setupDirectEventListeners();
48
+
49
+ // Initialiser l'audio
50
+ this.initAudio();
51
+ }
52
+
53
+ setupDirectEventListeners() {
54
+ // Stocker les listeners originaux pour pouvoir les retirer plus tard
55
+ this.originalListeners = {
56
+ mousedown: this.canvas.onmousedown,
57
+ mouseup: this.canvas.onmouseup,
58
+ mousemove: this.canvas.onmousemove,
59
+ touchstart: this.canvas.ontouchstart,
60
+ touchend: this.canvas.ontouchend,
61
+ touchmove: this.canvas.ontouchmove
62
+ };
63
+
64
+ // Attacher nos propres handlers
65
+ this.canvas.addEventListener('mousedown', this.handleCanvasMouseDown.bind(this));
66
+ this.canvas.addEventListener('mouseup', this.handleCanvasMouseUp.bind(this));
67
+ this.canvas.addEventListener('mousemove', this.handleCanvasMouseMove.bind(this));
68
+
69
+ this.canvas.addEventListener('touchstart', this.handleCanvasTouchStart.bind(this));
70
+ this.canvas.addEventListener('touchend', this.handleCanvasTouchEnd.bind(this));
71
+ this.canvas.addEventListener('touchmove', this.handleCanvasTouchMove.bind(this));
72
+ }
73
+
74
+ removeDirectEventListeners() {
75
+ // Retirer nos listeners
76
+ this.canvas.removeEventListener('mousedown', this.handleCanvasMouseDown);
77
+ this.canvas.removeEventListener('mouseup', this.handleCanvasMouseUp);
78
+ this.canvas.removeEventListener('mousemove', this.handleCanvasMouseMove);
79
+
80
+ this.canvas.removeEventListener('touchstart', this.handleCanvasTouchStart);
81
+ this.canvas.removeEventListener('touchend', this.handleCanvasTouchEnd);
82
+ this.canvas.removeEventListener('touchmove', this.handleCanvasTouchMove);
83
+
84
+ // Restaurer les listeners originaux
85
+ Object.keys(this.originalListeners).forEach(event => {
86
+ if (this.originalListeners[event]) {
87
+ this.canvas[`on${event}`] = this.originalListeners[event];
88
+ }
89
+ });
90
+ }
91
+
92
+ handleCanvasMouseDown(e) {
93
+ const rect = this.canvas.getBoundingClientRect();
94
+ const x = e.clientX - rect.left;
95
+ const y = e.clientY - rect.top;
96
+
97
+ if (this.isPointInside(x, y)) {
98
+ e.stopPropagation();
99
+ this.isMouseDownOnPlayer = true;
100
+ this.clickStartTime = Date.now();
101
+ this.handleInteraction(x, y, 'start');
102
+ return true; // Empêche la propagation au framework
103
+ }
104
+ }
105
+
106
+ handleCanvasMouseUp(e) {
107
+ if (!this.isMouseDownOnPlayer) return;
108
+
109
+ const rect = this.canvas.getBoundingClientRect();
110
+ const x = e.clientX - rect.left;
111
+ const y = e.clientY - rect.top;
112
+
113
+ const clickDuration = Date.now() - this.clickStartTime;
114
+
115
+ if (clickDuration < this.clickThreshold && this.isPointInside(x, y)) {
116
+ e.stopPropagation();
117
+ this.handleInteraction(x, y, 'end');
118
+ }
119
+
120
+ this.isMouseDownOnPlayer = false;
121
+ return true; // Empêche la propagation au framework
122
+ }
123
+
124
+ handleCanvasMouseMove(e) {
125
+ if (!this.isMouseDownOnPlayer) return;
126
+
127
+ const rect = this.canvas.getBoundingClientRect();
128
+ const x = e.clientX - rect.left;
129
+ const y = e.clientY - rect.top;
130
+
131
+ if (this.isPointInside(x, y)) {
132
+ e.stopPropagation();
133
+ this.handleInteraction(x, y, 'move');
134
+ return true;
135
+ }
136
+ }
137
+
138
+ handleCanvasTouchStart(e) {
139
+ const touch = e.touches[0];
140
+ const rect = this.canvas.getBoundingClientRect();
141
+ const x = touch.clientX - rect.left;
142
+ const y = touch.clientY - rect.top;
143
+
144
+ if (this.isPointInside(x, y)) {
145
+ e.stopPropagation();
146
+ e.preventDefault();
147
+ this.isMouseDownOnPlayer = true;
148
+ this.clickStartTime = Date.now();
149
+ this.handleInteraction(x, y, 'start');
150
+ return true;
151
+ }
152
+ }
153
+
154
+ handleCanvasTouchEnd(e) {
155
+ if (!this.isMouseDownOnPlayer) return;
156
+
157
+ const touch = e.changedTouches[0];
158
+ const rect = this.canvas.getBoundingClientRect();
159
+ const x = touch.clientX - rect.left;
160
+ const y = touch.clientY - rect.top;
161
+
162
+ const clickDuration = Date.now() - this.clickStartTime;
163
+
164
+ if (clickDuration < this.clickThreshold && this.isPointInside(x, y)) {
165
+ e.stopPropagation();
166
+ e.preventDefault();
167
+ this.handleInteraction(x, y, 'end');
168
+ }
169
+
170
+ this.isMouseDownOnPlayer = false;
171
+ return true;
172
+ }
173
+
174
+ handleCanvasTouchMove(e) {
175
+ if (!this.isMouseDownOnPlayer) return;
176
+
177
+ const touch = e.touches[0];
178
+ const rect = this.canvas.getBoundingClientRect();
179
+ const x = touch.clientX - rect.left;
180
+ const y = touch.clientY - rect.top;
181
+
182
+ if (this.isPointInside(x, y)) {
183
+ e.stopPropagation();
184
+ e.preventDefault();
185
+ this.handleInteraction(x, y, 'move');
186
+ return true;
187
+ }
188
+ }
189
+
190
+ handleInteraction(x, y, type) {
191
+ // Ajuster les coordonnées pour le scrolling
192
+ const adjustedY = y - this.framework.scrollOffset;
193
+
194
+ // Afficher les contrôles
195
+ this.showControls = true;
196
+ this.showControlsTemporarily();
197
+
198
+ // Marquer l'interaction utilisateur
199
+ if (!this.userInteracted) {
200
+ this.userInteracted = true;
201
+ this.showInteractionMessage = false;
202
+ }
203
+
204
+ // Calculer la position relative
205
+ const localX = x - this.x;
206
+ const localY = adjustedY - this.y;
207
+
208
+ // Bouton play/pause central
209
+ const centerX = this.width / 2;
210
+ const centerY = this.height / 2;
211
+
212
+ const buttonSize = 40;
213
+ const isInButton = localX >= centerX - buttonSize && localX <= centerX + buttonSize &&
214
+ localY >= centerY - buttonSize && localY <= centerY + buttonSize;
215
+
216
+ // Barre de progression
217
+ const progressBarY = this.height - 30;
218
+ const isInProgressBar = this.showControls && this.loaded &&
219
+ localY >= progressBarY && localY <= progressBarY + 15;
220
+
221
+ // Traiter selon le type d'événement
222
+ switch (type) {
223
+ case 'start':
224
+ // Marquer le début du clic
225
+ break;
226
+
227
+ case 'move':
228
+ if (isInProgressBar) {
229
+ const newProgress = Math.max(0, Math.min(100, (localX / this.width) * 100));
230
+ this.seekTo((this.duration * newProgress) / 100);
231
+ }
232
+ break;
233
+
234
+ case 'end':
235
+ // Bouton play/pause
236
+ if (isInButton) {
237
+ console.log('AudioPlayer: Bouton cliqué (gestion directe)');
238
+ if (!this.loaded && !this.isLoading) {
239
+ this.initAudio();
240
+ } else if (this.loaded) {
241
+ this.togglePlay();
242
+ }
243
+ }
244
+ break;
245
+ }
246
+ }
247
+
248
+ initAudio() {
249
+ if (!this.src) {
250
+ console.warn('AudioPlayer: Pas de source audio fournie');
251
+ return;
252
+ }
253
+
254
+ console.log('AudioPlayer: Initialisation de l\'audio:', this.src);
255
+
256
+ this.isLoading = true;
257
+
258
+ try {
259
+ // Créer l'élément audio
260
+ this.audioElement = document.createElement('audio');
261
+ this.audioElement.src = this.src;
262
+ this.audioElement.preload = 'auto';
263
+ this.audioElement.crossOrigin = 'anonymous';
264
+ this.audioElement.style.display = 'none';
265
+ this.audioElement.style.position = 'absolute';
266
+ this.audioElement.style.left = '-9999px';
267
+
268
+ this.audioElement.autoplay = false;
269
+ this.audioElement.controls = false;
270
+
271
+ document.body.appendChild(this.audioElement);
272
+
273
+ // Événements de l'audio
274
+ this.audioElement.addEventListener('loadedmetadata', () => {
275
+ console.log('AudioPlayer: Métadonnées chargées');
276
+ this.duration = this.audioElement.duration;
277
+ this.loaded = true;
278
+ this.isLoading = false;
279
+ });
280
+
281
+ this.audioElement.addEventListener('timeupdate', () => {
282
+ this.currentTime = this.audioElement.currentTime;
283
+ this.progress = (this.currentTime / this.duration) * 100;
284
+ });
285
+
286
+ this.audioElement.addEventListener('ended', () => {
287
+ console.log('AudioPlayer: Audio terminé');
288
+ this.playing = false;
289
+ if (this.onEnded) this.onEnded();
290
+ });
291
+
292
+ this.audioElement.addEventListener('play', () => {
293
+ console.log('AudioPlayer: Lecture démarrée');
294
+ this.playing = true;
295
+ if (this.onPlay) this.onPlay();
296
+ });
297
+
298
+ this.audioElement.addEventListener('pause', () => {
299
+ console.log('AudioPlayer: Lecture en pause');
300
+ this.playing = false;
301
+ if (this.onPause) this.onPause();
302
+ });
303
+
304
+ this.audioElement.addEventListener('error', (e) => {
305
+ console.error('AudioPlayer: Erreur audio:', e);
306
+ this.isLoading = false;
307
+ this.loaded = false;
308
+ });
309
+
310
+ this.audioElement.addEventListener('canplaythrough', () => {
311
+ console.log('AudioPlayer: Prêt à jouer');
312
+ this.loaded = true;
313
+ this.isLoading = false;
314
+ });
315
+
316
+ // Démarrer le chargement
317
+ this.audioElement.load();
318
+
319
+ } catch (error) {
320
+ console.error('AudioPlayer: Erreur d\'initialisation:', error);
321
+ this.isLoading = false;
322
+ this.loaded = false;
323
+ }
324
+ }
325
+
326
+ play() {
327
+ if (!this.loaded || !this.audioElement) {
328
+ console.log('AudioPlayer: Impossible de jouer - pas chargé');
329
+ return;
330
+ }
331
+
332
+ // Vérifier l'interaction utilisateur
333
+ if (!this.userInteracted) {
334
+ console.log('AudioPlayer: Attente interaction utilisateur...');
335
+ this.showInteractionMessage = true;
336
+ setTimeout(() => {
337
+ this.showInteractionMessage = false;
338
+ }, 2000);
339
+ return;
340
+ }
341
+
342
+ console.log('AudioPlayer: Lancement de la lecture');
343
+
344
+ const playPromise = this.audioElement.play();
345
+
346
+ if (playPromise !== undefined) {
347
+ playPromise
348
+ .then(() => {
349
+ console.log('AudioPlayer: Lecture réussie');
350
+ this.playing = true;
351
+ this.showControlsTemporarily();
352
+ })
353
+ .catch(error => {
354
+ console.error('AudioPlayer: Erreur de lecture:', error);
355
+ this.playing = false;
356
+
357
+ if (error.name === 'NotAllowedError') {
358
+ console.log('AudioPlayer: Interaction nécessaire');
359
+ this.userInteracted = false;
360
+ }
361
+ });
362
+ }
363
+ }
364
+
365
+ pause() {
366
+ if (!this.playing || !this.audioElement) return;
367
+
368
+ console.log('AudioPlayer: Mise en pause');
369
+ this.audioElement.pause();
370
+ this.playing = false;
371
+ this.showControlsTemporarily();
372
+ }
373
+
374
+ stop() {
375
+ if (this.audioElement) {
376
+ this.audioElement.pause();
377
+ this.audioElement.currentTime = 0;
378
+ }
379
+ this.playing = false;
380
+ this.currentTime = 0;
381
+ this.progress = 0;
382
+ }
383
+
384
+ togglePlay() {
385
+ console.log('AudioPlayer: togglePlay - état actuel:', this.playing);
386
+
387
+ if (!this.loaded) {
388
+ console.log('AudioPlayer: Pas encore chargé');
389
+ if (!this.isLoading) {
390
+ this.initAudio();
391
+ }
392
+ return;
393
+ }
394
+
395
+ if (!this.userInteracted) {
396
+ console.log('AudioPlayer: Interaction utilisateur déclenchée');
397
+ this.userInteracted = true;
398
+ this.showInteractionMessage = false;
399
+ }
400
+
401
+ if (this.playing) {
402
+ this.pause();
403
+ } else {
404
+ this.play();
405
+ }
406
+ }
407
+
408
+ seekTo(time) {
409
+ if (!this.loaded || !this.audioElement) return;
410
+
411
+ const wasPlaying = this.playing;
412
+
413
+ if (wasPlaying) {
414
+ this.audioElement.pause();
415
+ }
416
+
417
+ const newTime = Math.max(0, Math.min(time, this.duration));
418
+ this.audioElement.currentTime = newTime;
419
+ this.currentTime = newTime;
420
+ this.progress = (this.currentTime / this.duration) * 100;
421
+
422
+ if (wasPlaying) {
423
+ this.audioElement.play().catch(e => {
424
+ console.error('AudioPlayer: Erreur reprise après seek:', e);
425
+ });
426
+ }
427
+ }
428
+
429
+ setVolume(value) {
430
+ this.volume = Math.max(0, Math.min(1, value));
431
+ if (this.audioElement) {
432
+ this.audioElement.volume = this.volume;
433
+ }
434
+ }
435
+
436
+ showControlsTemporarily() {
437
+ this.showControls = true;
438
+ clearTimeout(this.controlsTimeout);
439
+ this.controlsTimeout = setTimeout(() => {
440
+ if (this.playing) this.showControls = false;
441
+ }, 3000);
442
+ }
443
+
444
+ update(deltaTime) {
445
+ // Mise à jour automatique via timeupdate
446
+ }
447
+
448
+ draw(ctx) {
449
+ ctx.save();
450
+
451
+ // Fond du lecteur
452
+ ctx.fillStyle = '#111111';
453
+ ctx.fillRect(this.x, this.y, this.width, this.height);
454
+
455
+ // Message d'interaction
456
+ if (this.showInteractionMessage) {
457
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
458
+ ctx.font = 'bold 14px Arial';
459
+ ctx.textAlign = 'center';
460
+ ctx.textBaseline = 'middle';
461
+ ctx.fillText('Cliquez pour activer l\'audio', this.x + this.width / 2, this.y + this.height / 2);
462
+ ctx.restore();
463
+ return;
464
+ }
465
+
466
+ // Indicateur de chargement
467
+ if (this.isLoading) {
468
+ ctx.fillStyle = '#FFFFFF';
469
+ ctx.font = '14px Arial';
470
+ ctx.textAlign = 'center';
471
+ ctx.textBaseline = 'middle';
472
+ ctx.fillText('Chargement...', this.x + this.width / 2, this.y + this.height / 2);
473
+ ctx.restore();
474
+ return;
475
+ }
476
+
477
+ // Indicateur si non chargé
478
+ if (!this.loaded && !this.isLoading) {
479
+ ctx.fillStyle = '#FFFFFF';
480
+ ctx.font = '14px Arial';
481
+ ctx.textAlign = 'center';
482
+ ctx.textBaseline = 'middle';
483
+ ctx.fillText('Audio non disponible', this.x + this.width / 2, this.y + this.height / 2);
484
+ ctx.restore();
485
+ return;
486
+ }
487
+
488
+ // Bouton play/pause
489
+ const centerX = this.x + this.width / 2;
490
+ const centerY = this.y + this.height / 2;
491
+
492
+ if (!this.playing || this.showControls) {
493
+ // Cercle de fond
494
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
495
+ ctx.beginPath();
496
+ ctx.arc(centerX, centerY, 40, 0, Math.PI * 2);
497
+ ctx.fill();
498
+
499
+ // Icône
500
+ ctx.fillStyle = '#FFFFFF';
501
+ if (this.playing) {
502
+ // Icône pause
503
+ const barWidth = 8;
504
+ const barHeight = 30;
505
+ const gap = 5;
506
+
507
+ ctx.fillRect(centerX - barWidth - gap/2, centerY - barHeight/2, barWidth, barHeight);
508
+ ctx.fillRect(centerX + gap/2, centerY - barHeight/2, barWidth, barHeight);
509
+ } else {
510
+ // Icône play
511
+ const triangleSize = 25;
512
+ ctx.beginPath();
513
+ ctx.moveTo(centerX - triangleSize/2, centerY - triangleSize);
514
+ ctx.lineTo(centerX - triangleSize/2, centerY + triangleSize);
515
+ ctx.lineTo(centerX + triangleSize, centerY);
516
+ ctx.closePath();
517
+ ctx.fill();
518
+ }
519
+ }
520
+
521
+ // Contrôles en bas
522
+ if (this.showControls && this.loaded) {
523
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
524
+ ctx.fillRect(this.x, this.y + this.height - this.controlsHeight, this.width, this.controlsHeight);
525
+
526
+ const progressX = this.x + 15;
527
+ const progressY = this.y + this.height - 25;
528
+ const progressWidth = this.width - 30;
529
+
530
+ // Barre de progression
531
+ ctx.fillStyle = '#555555';
532
+ ctx.fillRect(progressX, progressY, progressWidth, 6);
533
+ ctx.fillStyle = '#333333';
534
+ ctx.fillRect(progressX, progressY, progressWidth, 2);
535
+
536
+ // Progression actuelle
537
+ ctx.fillStyle = '#FF4444';
538
+ const currentProgressWidth = (progressWidth * this.progress) / 100;
539
+ ctx.fillRect(progressX, progressY, currentProgressWidth, 6);
540
+ ctx.fillStyle = '#FF0000';
541
+ ctx.fillRect(progressX, progressY, currentProgressWidth, 2);
542
+
543
+ // Curseur
544
+ if (currentProgressWidth > 0) {
545
+ const thumbX = progressX + currentProgressWidth;
546
+ ctx.fillStyle = '#FFFFFF';
547
+ ctx.beginPath();
548
+ ctx.arc(thumbX, progressY + 3, 8, 0, Math.PI * 2);
549
+ ctx.fill();
550
+
551
+ ctx.strokeStyle = '#000000';
552
+ ctx.lineWidth = 2;
553
+ ctx.stroke();
554
+ }
555
+
556
+ // Temps
557
+ const currentTimeStr = this.formatTime(this.currentTime);
558
+ const totalTimeStr = this.formatTime(this.duration);
559
+
560
+ ctx.fillStyle = '#FFFFFF';
561
+ ctx.font = 'bold 12px Arial';
562
+ ctx.textAlign = 'left';
563
+ ctx.fillText(currentTimeStr, this.x + 15, this.y + this.height - 35);
564
+
565
+ ctx.textAlign = 'right';
566
+ ctx.fillText(totalTimeStr, this.x + this.width - 15, this.y + this.height - 35);
567
+ }
568
+
569
+ ctx.restore();
570
+ }
571
+
572
+ formatTime(seconds) {
573
+ if (isNaN(seconds) || seconds === Infinity) {
574
+ return "0:00";
575
+ }
576
+
577
+ const mins = Math.floor(seconds / 60);
578
+ const secs = Math.floor(seconds % 60);
579
+ return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
580
+ }
581
+
582
+ isPointInside(x, y) {
583
+ const adjustedY = y - this.framework.scrollOffset;
584
+ return x >= this.x && x <= this.x + this.width &&
585
+ adjustedY >= this.y && adjustedY <= this.y + this.height;
586
+ }
587
+
588
+ remove() {
589
+ // Arrêter la lecture
590
+ this.stop();
591
+
592
+ // Retirer les event listeners
593
+ this.removeDirectEventListeners();
594
+
595
+ // Supprimer l'élément audio
596
+ if (this.audioElement && this.audioElement.parentNode) {
597
+ this.audioElement.pause();
598
+ this.audioElement.src = '';
599
+ this.audioElement.load();
600
+ this.audioElement.parentNode.removeChild(this.audioElement);
601
+ this.audioElement = null;
602
+ }
603
+
604
+ // Nettoyer le timeout
605
+ clearTimeout(this.controlsTimeout);
606
+
607
+ console.log('AudioPlayer: Nettoyé et supprimé');
608
+ }
609
+ }
610
+
611
+ export default AudioPlayer;
@@ -53,6 +53,7 @@ import InputDatalist from '../components/InputDatalist.js';
53
53
  import Banner from '../components/Banner.js';
54
54
  import Chart from '../components/Chart.js';
55
55
  import SliverAppBar from '../components/SliverAppBar.js';
56
+ import AudioPlayer from '../components/AudioPlayer.js';
56
57
 
57
58
  // Utils
58
59
  import SafeArea from '../utils/SafeArea.js';
@@ -177,8 +178,22 @@ class CanvasFramework {
177
178
  this.lightTheme = lightTheme;
178
179
  this.darkTheme = darkTheme;
179
180
  this.theme = lightTheme; // thème par défaut
180
-
181
+ // État actuel + préférence
182
+ this.themeMode = options.themeMode || 'system'; // 'light', 'dark', 'system'
183
+ this.userThemeOverride = null; // null = suit system, sinon 'light' ou 'dark'
184
+
185
+ // Applique le thème initial
186
+ this.setupSystemThemeListener();
187
+
188
+ // Récupère override utilisateur
189
+ const savedOverride = localStorage.getItem('themeOverride');
190
+ if (savedOverride && ['light', 'dark'].includes(savedOverride)) {
191
+ this.userThemeOverride = savedOverride;
192
+ this.themeMode = savedOverride;
193
+ }
194
+
181
195
  this.components = [];
196
+ this.applyThemeFromSystem();
182
197
  this.state = {};
183
198
  // NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
184
199
  this.useWebGL = options.useWebGL !== false; // true par défaut
@@ -264,6 +279,70 @@ class CanvasFramework {
264
279
  }
265
280
  }
266
281
 
282
+ /**
283
+ * Détecte le thème système et applique si mode = 'system'
284
+ */
285
+ applyThemeFromSystem() {
286
+ if (this.themeMode === 'system') {
287
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
288
+ const newTheme = prefersDark ? this.darkTheme : this.lightTheme;
289
+ this.setTheme(newTheme);
290
+ } else {
291
+ // Mode forcé
292
+ this.setTheme(
293
+ this.themeMode === 'dark' ? this.darkTheme : this.lightTheme
294
+ );
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Écoute les changements système (ex: utilisateur bascule dark mode)
300
+ */
301
+ setupSystemThemeListener() {
302
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
303
+
304
+ // Ancienne méthode (compatibilité large)
305
+ if (mediaQuery.addEventListener) {
306
+ mediaQuery.addEventListener('change', (e) => {
307
+ if (this.themeMode === 'system') {
308
+ this.applyThemeFromSystem();
309
+ }
310
+ });
311
+ } else {
312
+ // Anciens navigateurs (rare en 2026)
313
+ mediaQuery.addListener((e) => {
314
+ if (this.themeMode === 'system') {
315
+ this.applyThemeFromSystem();
316
+ }
317
+ });
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Change le mode thème
323
+ * @param {'light'|'dark'|'system'} mode - Mode à appliquer
324
+ * @param {boolean} [save=true] - Sauvegarder le choix utilisateur ?
325
+ */
326
+ setThemeMode(mode, save = true) {
327
+ if (!['light', 'dark', 'system'].includes(mode)) {
328
+ console.warn('Mode invalide, valeurs acceptées: light, dark, system');
329
+ return;
330
+ }
331
+
332
+ this.themeMode = mode;
333
+
334
+ if (save && mode !== 'system') {
335
+ this.userThemeOverride = mode;
336
+ // Sauvegarde (ex: localStorage ou ton SecureStorage)
337
+ localStorage.setItem('themeOverride', mode);
338
+ } else if (mode === 'system') {
339
+ this.userThemeOverride = null;
340
+ localStorage.removeItem('themeOverride');
341
+ }
342
+
343
+ this.applyThemeFromSystem();
344
+ }
345
+
267
346
  /**
268
347
  * Active ou désactive les DevTools
269
348
  * @param {boolean} enabled - true pour activer, false pour désactiver
@@ -430,13 +509,16 @@ class CanvasFramework {
430
509
  setTheme(theme) {
431
510
  this.theme = theme;
432
511
 
433
- // Intercepter le context pour remplacer les couleurs globalement
434
512
  if (!this.useWebGL) {
435
513
  this.wrapContext(this.ctx, theme);
436
514
  }
437
515
 
438
- // marque tous les composants dirty pour redraw
439
- for (let comp of this.components) comp.markDirty();
516
+ // Protège la boucle
517
+ if (this.components && Array.isArray(this.components)) {
518
+ this.components.forEach(comp => comp.markDirty());
519
+ } else {
520
+ console.warn('[setTheme] components pas encore initialisé');
521
+ }
440
522
  }
441
523
 
442
524
  // Switch Theme
package/core/UIBuilder.js CHANGED
@@ -54,6 +54,7 @@ import InputDatalist from '../components/InputDatalist.js';
54
54
  import Banner from '../components/Banner.js';
55
55
  import Chart from '../components/Chart.js';
56
56
  import SliverAppBar from '../components/SliverAppBar.js';
57
+ import AudioPlayer from '../components/AudioPlayer.js';
57
58
 
58
59
  // Features
59
60
  import PullToRefresh from '../features/PullToRefresh.js';
@@ -118,6 +119,7 @@ const Components = {
118
119
  Divider,
119
120
  FileUpload,
120
121
  Table,
122
+ AudioPlayer,
121
123
  TreeView,
122
124
  SearchInput,
123
125
  ImageCarousel,
package/index.js CHANGED
@@ -61,6 +61,7 @@ export { default as InputDatalist } from './components/InputDatalist.js';
61
61
  export { default as Banner } from './components/Banner.js';
62
62
  export { default as Chart } from './components/Chart.js';
63
63
  export { default as SliverAppBar } from './components/SliverAppBar.js';
64
+ export { default as AudioPlayer } from './components/AudioPlayer.js';
64
65
 
65
66
  // Utils
66
67
  export { default as SafeArea } from './utils/SafeArea.js';
@@ -103,7 +104,7 @@ export { default as FeatureFlags } from './manager/FeatureFlags.js';
103
104
 
104
105
  // Version du framework
105
106
 
106
- export const VERSION = '0.3.23';
107
+ export const VERSION = '0.3.24';
107
108
 
108
109
 
109
110
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "description": "Canvas-based cross-platform UI framework (Material & Cupertino)",
5
5
  "type": "module",
6
6
  "main": "./index.js",