canvasframework 0.3.24 → 0.3.26
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.
- package/components/Camera.js +392 -0
- package/core/CanvasFramework.js +86 -4
- package/core/UIBuilder.js +2 -0
- package/index.js +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Composant Camera autonome avec gestion directe des clics/touches
|
|
5
|
+
* Modes : contain (tout visible + bandes), cover (remplit + crop), fit (centre sans crop)
|
|
6
|
+
*/
|
|
7
|
+
class Camera extends Component {
|
|
8
|
+
constructor(framework, options = {}) {
|
|
9
|
+
super(framework, options);
|
|
10
|
+
|
|
11
|
+
this.facingMode = options.facingMode || 'environment';
|
|
12
|
+
this.autoStart = options.autoStart !== false;
|
|
13
|
+
this.onPhoto = options.onPhoto || null;
|
|
14
|
+
|
|
15
|
+
this.fitModes = ['contain', 'cover'];
|
|
16
|
+
this.currentFitModeIndex = 0;
|
|
17
|
+
this.fitMode = this.fitModes[this.currentFitModeIndex];
|
|
18
|
+
|
|
19
|
+
this.stream = null;
|
|
20
|
+
this.video = null;
|
|
21
|
+
this.loaded = false;
|
|
22
|
+
this.error = null;
|
|
23
|
+
|
|
24
|
+
this.showControls = true;
|
|
25
|
+
this.torchSupported = false;
|
|
26
|
+
this.torchOn = false;
|
|
27
|
+
this.captureButtonRadius = 35;
|
|
28
|
+
this.modeButtonSize = 40;
|
|
29
|
+
|
|
30
|
+
// Feedback capture
|
|
31
|
+
this.flashTimer = null;
|
|
32
|
+
this.previewPhoto = null; // dataUrl dernière photo
|
|
33
|
+
this.previewTimeout = null;
|
|
34
|
+
|
|
35
|
+
this.isStarting = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async _mount() {
|
|
39
|
+
super._mount?.();
|
|
40
|
+
|
|
41
|
+
// Redémarrage auto si besoin
|
|
42
|
+
if (this.autoStart && !this.stream && !this.isStarting) {
|
|
43
|
+
this.isStarting = true;
|
|
44
|
+
await this.startCamera();
|
|
45
|
+
this.isStarting = false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.setupEventListeners();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
destroy() {
|
|
52
|
+
this.removeEventListeners();
|
|
53
|
+
this.stopCamera();
|
|
54
|
+
if (this.flashTimer) clearTimeout(this.flashTimer);
|
|
55
|
+
if (this.previewTimeout) clearTimeout(this.previewTimeout);
|
|
56
|
+
super.destroy?.();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Écoute directe (indépendante du framework)
|
|
60
|
+
setupEventListeners() {
|
|
61
|
+
this.onTouchStart = this.handleTouchStart.bind(this);
|
|
62
|
+
this.onTouchMove = this.handleTouchMove.bind(this);
|
|
63
|
+
this.onTouchEnd = this.handleTouchEnd.bind(this);
|
|
64
|
+
this.onMouseDown = this.handleMouseDown.bind(this);
|
|
65
|
+
this.onMouseMove = this.handleMouseMove.bind(this);
|
|
66
|
+
this.onMouseUp = this.handleMouseUp.bind(this);
|
|
67
|
+
|
|
68
|
+
const canvas = this.framework.canvas;
|
|
69
|
+
canvas.addEventListener('touchstart', this.onTouchStart, { passive: false });
|
|
70
|
+
canvas.addEventListener('touchmove', this.onTouchMove, { passive: false });
|
|
71
|
+
canvas.addEventListener('touchend', this.onTouchEnd, { passive: false });
|
|
72
|
+
canvas.addEventListener('mousedown', this.onMouseDown);
|
|
73
|
+
canvas.addEventListener('mousemove', this.onMouseMove);
|
|
74
|
+
canvas.addEventListener('mouseup', this.onMouseUp);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
removeEventListeners() {
|
|
78
|
+
const canvas = this.framework.canvas;
|
|
79
|
+
canvas.removeEventListener('touchstart', this.onTouchStart);
|
|
80
|
+
canvas.removeEventListener('touchmove', this.onTouchMove);
|
|
81
|
+
canvas.removeEventListener('touchend', this.onTouchEnd);
|
|
82
|
+
canvas.removeEventListener('mousedown', this.onMouseDown);
|
|
83
|
+
canvas.removeEventListener('mousemove', this.onMouseMove);
|
|
84
|
+
canvas.removeEventListener('mouseup', this.onMouseUp);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Coordonnées locales
|
|
88
|
+
getLocalPos(clientX, clientY) {
|
|
89
|
+
const rect = this.framework.canvas.getBoundingClientRect();
|
|
90
|
+
const globalX = clientX - rect.left;
|
|
91
|
+
const globalY = clientY - rect.top - this.framework.scrollOffset;
|
|
92
|
+
return {
|
|
93
|
+
x: globalX - this.x,
|
|
94
|
+
y: globalY - this.y
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
handleTouchStart(e) {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
const touch = e.touches[0];
|
|
101
|
+
const pos = this.getLocalPos(touch.clientX, touch.clientY);
|
|
102
|
+
this.handlePress(pos.x, pos.y);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
handleTouchMove(e) {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
handleTouchEnd(e) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
handleMouseDown(e) {
|
|
114
|
+
const pos = this.getLocalPos(e.clientX, e.clientY);
|
|
115
|
+
this.handlePress(pos.x, pos.y);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
handleMouseMove(e) {}
|
|
119
|
+
|
|
120
|
+
handleMouseUp(e) {}
|
|
121
|
+
|
|
122
|
+
async startCamera() {
|
|
123
|
+
if (this.stream) return;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
this.stopCamera();
|
|
127
|
+
|
|
128
|
+
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
129
|
+
video: {
|
|
130
|
+
facingMode: this.facingMode,
|
|
131
|
+
width: { ideal: 1280 },
|
|
132
|
+
height: { ideal: 720 },
|
|
133
|
+
frameRate: { ideal: 30 }
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const [track] = this.stream.getVideoTracks();
|
|
138
|
+
const caps = track.getCapabilities?.() || {};
|
|
139
|
+
this.torchSupported = !!caps.torch;
|
|
140
|
+
|
|
141
|
+
this.video = document.createElement('video');
|
|
142
|
+
this.video.autoplay = true;
|
|
143
|
+
this.video.playsInline = true;
|
|
144
|
+
this.video.muted = true;
|
|
145
|
+
this.video.srcObject = this.stream;
|
|
146
|
+
|
|
147
|
+
this.video.style.position = 'fixed';
|
|
148
|
+
this.video.style.left = '-9999px';
|
|
149
|
+
this.video.style.top = '-9999px';
|
|
150
|
+
this.video.style.width = '1px';
|
|
151
|
+
this.video.style.height = '1px';
|
|
152
|
+
document.body.appendChild(this.video);
|
|
153
|
+
|
|
154
|
+
await new Promise(resolve => {
|
|
155
|
+
this.video.onloadedmetadata = () => {
|
|
156
|
+
this.loaded = true;
|
|
157
|
+
this.video.play().catch(e => console.warn('Play auto échoué:', e));
|
|
158
|
+
resolve();
|
|
159
|
+
};
|
|
160
|
+
this.video.onerror = (e) => {
|
|
161
|
+
this.error = 'Erreur vidéo';
|
|
162
|
+
console.error('Video error:', e);
|
|
163
|
+
resolve();
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this.markDirty();
|
|
168
|
+
} catch (err) {
|
|
169
|
+
this.error = err.message || 'Accès caméra refusé';
|
|
170
|
+
console.error('Échec getUserMedia:', err);
|
|
171
|
+
this.markDirty();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
stopCamera() {
|
|
176
|
+
if (this.stream) {
|
|
177
|
+
this.stream.getTracks().forEach(track => track.stop());
|
|
178
|
+
this.stream = null;
|
|
179
|
+
}
|
|
180
|
+
if (this.video) {
|
|
181
|
+
if (this.video.parentNode) {
|
|
182
|
+
this.video.parentNode.removeChild(this.video);
|
|
183
|
+
}
|
|
184
|
+
this.video.srcObject = null;
|
|
185
|
+
this.video = null;
|
|
186
|
+
}
|
|
187
|
+
this.loaded = false;
|
|
188
|
+
this.markDirty();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async switchCamera() {
|
|
192
|
+
this.stopCamera();
|
|
193
|
+
this.facingMode = this.facingMode === 'user' ? 'environment' : 'user';
|
|
194
|
+
await this.startCamera();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async toggleTorch() {
|
|
198
|
+
if (!this.torchSupported || !this.stream) return;
|
|
199
|
+
|
|
200
|
+
const [track] = this.stream.getVideoTracks();
|
|
201
|
+
try {
|
|
202
|
+
await track.applyConstraints({
|
|
203
|
+
advanced: [{ torch: !this.torchOn }]
|
|
204
|
+
});
|
|
205
|
+
this.torchOn = !this.torchOn;
|
|
206
|
+
this.markDirty();
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.warn('Torch impossible:', err);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
capturePhoto() {
|
|
213
|
+
if (!this.loaded || !this.video) {
|
|
214
|
+
console.warn('Capture impossible : caméra pas prête');
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const canvas = document.createElement('canvas');
|
|
219
|
+
canvas.width = this.video.videoWidth;
|
|
220
|
+
canvas.height = this.video.videoHeight;
|
|
221
|
+
|
|
222
|
+
const ctx = canvas.getContext('2d');
|
|
223
|
+
ctx.drawImage(this.video, 0, 0);
|
|
224
|
+
|
|
225
|
+
const dataUrl = canvas.toDataURL('image/jpeg', 0.92);
|
|
226
|
+
if (this.onPhoto) this.onPhoto(dataUrl);
|
|
227
|
+
console.log('Photo capturée ! DataURL:', dataUrl.substring(0, 50) + '...');
|
|
228
|
+
|
|
229
|
+
// Feedback : flash + preview 3s
|
|
230
|
+
this.flashTimer = setTimeout(() => {
|
|
231
|
+
this.flashTimer = null;
|
|
232
|
+
this.markDirty();
|
|
233
|
+
}, 200);
|
|
234
|
+
|
|
235
|
+
this.previewPhoto = dataUrl;
|
|
236
|
+
this.previewTimeout = setTimeout(() => {
|
|
237
|
+
this.previewPhoto = null;
|
|
238
|
+
this.markDirty();
|
|
239
|
+
}, 3000);
|
|
240
|
+
|
|
241
|
+
this.markDirty();
|
|
242
|
+
return dataUrl;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
switchFitMode() {
|
|
246
|
+
this.currentFitModeIndex = (this.currentFitModeIndex + 1) % this.fitModes.length;
|
|
247
|
+
this.fitMode = this.fitModes[this.currentFitModeIndex];
|
|
248
|
+
this.markDirty();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
handlePress(relX, relY) {
|
|
252
|
+
|
|
253
|
+
// Capture centrale
|
|
254
|
+
const captureX = this.width / 2;
|
|
255
|
+
const captureY = this.height - 60;
|
|
256
|
+
if (Math.hypot(relX - captureX, relY - captureY) < this.captureButtonRadius + 10) {
|
|
257
|
+
this.capturePhoto();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Switch caméra
|
|
262
|
+
if (relX < 60 && relY < 60) {
|
|
263
|
+
this.switchCamera();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Torch
|
|
268
|
+
if (this.torchSupported && relX > this.width - 60 && relY < 60) {
|
|
269
|
+
this.toggleTorch();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Switch mode
|
|
274
|
+
const modeButtonX = this.width - 80;
|
|
275
|
+
const modeButtonY = 20;
|
|
276
|
+
if (relX > modeButtonX && relX < modeButtonX + this.modeButtonSize &&
|
|
277
|
+
relY > modeButtonY && relY < modeButtonY + this.modeButtonSize) {
|
|
278
|
+
this.switchFitMode();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
draw(ctx) {
|
|
284
|
+
ctx.save();
|
|
285
|
+
|
|
286
|
+
ctx.fillStyle = '#000';
|
|
287
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
288
|
+
|
|
289
|
+
// Flash blanc après capture
|
|
290
|
+
if (this.flashTimer) {
|
|
291
|
+
ctx.fillStyle = 'rgba(255,255,255,0.6)';
|
|
292
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (this.error) {
|
|
296
|
+
ctx.fillStyle = '#ff4444';
|
|
297
|
+
ctx.font = '16px Arial';
|
|
298
|
+
ctx.textAlign = 'center';
|
|
299
|
+
ctx.textBaseline = 'middle';
|
|
300
|
+
ctx.fillText(this.error, this.x + this.width/2, this.y + this.height/2);
|
|
301
|
+
} else if (!this.loaded) {
|
|
302
|
+
ctx.fillStyle = '#fff';
|
|
303
|
+
ctx.font = '16px Arial';
|
|
304
|
+
ctx.textAlign = 'center';
|
|
305
|
+
ctx.textBaseline = 'middle';
|
|
306
|
+
ctx.fillText('Démarrage caméra...', this.x + this.width/2, this.y + this.height/2);
|
|
307
|
+
} else if (this.video && this.loaded) {
|
|
308
|
+
const videoRatio = this.video.videoWidth / this.video.videoHeight;
|
|
309
|
+
const canvasRatio = this.width / this.height;
|
|
310
|
+
|
|
311
|
+
let drawWidth = this.width;
|
|
312
|
+
let drawHeight = this.height;
|
|
313
|
+
let offsetX = 0;
|
|
314
|
+
let offsetY = 0;
|
|
315
|
+
|
|
316
|
+
if (this.fitMode === 'cover') {
|
|
317
|
+
if (videoRatio > canvasRatio) {
|
|
318
|
+
drawHeight = this.height;
|
|
319
|
+
drawWidth = drawHeight * videoRatio;
|
|
320
|
+
offsetX = (this.width - drawWidth) / 2;
|
|
321
|
+
} else {
|
|
322
|
+
drawWidth = this.width;
|
|
323
|
+
drawHeight = drawWidth / videoRatio;
|
|
324
|
+
offsetY = (this.height - drawHeight) / 2;
|
|
325
|
+
}
|
|
326
|
+
} else if (this.fitMode === 'contain') {
|
|
327
|
+
if (videoRatio > canvasRatio) {
|
|
328
|
+
drawWidth = this.width;
|
|
329
|
+
drawHeight = drawWidth / videoRatio;
|
|
330
|
+
offsetY = (this.height - drawHeight) / 2;
|
|
331
|
+
} else {
|
|
332
|
+
drawHeight = this.height;
|
|
333
|
+
drawWidth = drawHeight * videoRatio;
|
|
334
|
+
offsetX = (this.width - drawWidth) / 2;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
ctx.drawImage(this.video, this.x + offsetX, this.y + offsetY, drawWidth, drawHeight);
|
|
339
|
+
|
|
340
|
+
// Mini preview dernière photo (bas droite, 3s)
|
|
341
|
+
if (this.previewPhoto) {
|
|
342
|
+
const previewSize = 80;
|
|
343
|
+
ctx.drawImage(this.previewPhoto, this.x + this.width - previewSize - 10, this.y + this.height - previewSize - 10, previewSize, previewSize);
|
|
344
|
+
ctx.strokeStyle = '#fff';
|
|
345
|
+
ctx.lineWidth = 2;
|
|
346
|
+
ctx.strokeRect(this.x + this.width - previewSize - 10, this.y + this.height - previewSize - 10, previewSize, previewSize);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Contrôles bas
|
|
351
|
+
ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
|
352
|
+
ctx.fillRect(this.x, this.y + this.height - 100, this.width, 100);
|
|
353
|
+
|
|
354
|
+
// Bouton capture
|
|
355
|
+
ctx.fillStyle = '#ffffff';
|
|
356
|
+
ctx.beginPath();
|
|
357
|
+
ctx.arc(this.x + this.width/2, this.y + this.height - 50, this.captureButtonRadius, 0, Math.PI * 2);
|
|
358
|
+
ctx.fill();
|
|
359
|
+
|
|
360
|
+
ctx.strokeStyle = '#ff4444';
|
|
361
|
+
ctx.lineWidth = 6;
|
|
362
|
+
ctx.beginPath();
|
|
363
|
+
ctx.arc(this.x + this.width/2, this.y + this.height - 50, this.captureButtonRadius + 10, 0, Math.PI * 2);
|
|
364
|
+
ctx.stroke();
|
|
365
|
+
|
|
366
|
+
// Switch caméra
|
|
367
|
+
ctx.fillStyle = '#ffffff';
|
|
368
|
+
ctx.font = '28px Arial';
|
|
369
|
+
ctx.fillText('↻', this.x + 20, this.y + 45);
|
|
370
|
+
|
|
371
|
+
// Torch
|
|
372
|
+
if (this.torchSupported) {
|
|
373
|
+
ctx.fillStyle = this.torchOn ? '#ffeb3b' : '#ffffff';
|
|
374
|
+
ctx.fillText('⚡', this.x + this.width - 50, this.y + 45);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Bouton switch mode
|
|
378
|
+
ctx.fillStyle = '#ffffff';
|
|
379
|
+
ctx.fillRect(this.x + this.width - 80, this.y + 20, this.modeButtonSize, this.modeButtonSize);
|
|
380
|
+
ctx.fillStyle = '#000';
|
|
381
|
+
ctx.font = '14px Arial';
|
|
382
|
+
ctx.textAlign = 'center';
|
|
383
|
+
ctx.textBaseline = 'middle';
|
|
384
|
+
ctx.fillText(this.fitMode.toUpperCase().slice(0, 4),
|
|
385
|
+
this.x + this.width - 80 + this.modeButtonSize/2,
|
|
386
|
+
this.y + 20 + this.modeButtonSize/2);
|
|
387
|
+
|
|
388
|
+
ctx.restore();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export default Camera;
|
package/core/CanvasFramework.js
CHANGED
|
@@ -54,6 +54,7 @@ import Banner from '../components/Banner.js';
|
|
|
54
54
|
import Chart from '../components/Chart.js';
|
|
55
55
|
import SliverAppBar from '../components/SliverAppBar.js';
|
|
56
56
|
import AudioPlayer from '../components/AudioPlayer.js';
|
|
57
|
+
import Camera from '../components/Camera.js';
|
|
57
58
|
|
|
58
59
|
// Utils
|
|
59
60
|
import SafeArea from '../utils/SafeArea.js';
|
|
@@ -178,8 +179,22 @@ class CanvasFramework {
|
|
|
178
179
|
this.lightTheme = lightTheme;
|
|
179
180
|
this.darkTheme = darkTheme;
|
|
180
181
|
this.theme = lightTheme; // thème par défaut
|
|
181
|
-
|
|
182
|
+
// État actuel + préférence
|
|
183
|
+
this.themeMode = options.themeMode || 'system'; // 'light', 'dark', 'system'
|
|
184
|
+
this.userThemeOverride = null; // null = suit system, sinon 'light' ou 'dark'
|
|
185
|
+
|
|
186
|
+
// Applique le thème initial
|
|
187
|
+
this.setupSystemThemeListener();
|
|
188
|
+
|
|
189
|
+
// Récupère override utilisateur
|
|
190
|
+
const savedOverride = localStorage.getItem('themeOverride');
|
|
191
|
+
if (savedOverride && ['light', 'dark'].includes(savedOverride)) {
|
|
192
|
+
this.userThemeOverride = savedOverride;
|
|
193
|
+
this.themeMode = savedOverride;
|
|
194
|
+
}
|
|
195
|
+
|
|
182
196
|
this.components = [];
|
|
197
|
+
this.applyThemeFromSystem();
|
|
183
198
|
this.state = {};
|
|
184
199
|
// NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
|
|
185
200
|
this.useWebGL = options.useWebGL !== false; // true par défaut
|
|
@@ -265,6 +280,70 @@ class CanvasFramework {
|
|
|
265
280
|
}
|
|
266
281
|
}
|
|
267
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Détecte le thème système et applique si mode = 'system'
|
|
285
|
+
*/
|
|
286
|
+
applyThemeFromSystem() {
|
|
287
|
+
if (this.themeMode === 'system') {
|
|
288
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
289
|
+
const newTheme = prefersDark ? this.darkTheme : this.lightTheme;
|
|
290
|
+
this.setTheme(newTheme);
|
|
291
|
+
} else {
|
|
292
|
+
// Mode forcé
|
|
293
|
+
this.setTheme(
|
|
294
|
+
this.themeMode === 'dark' ? this.darkTheme : this.lightTheme
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Écoute les changements système (ex: utilisateur bascule dark mode)
|
|
301
|
+
*/
|
|
302
|
+
setupSystemThemeListener() {
|
|
303
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
304
|
+
|
|
305
|
+
// Ancienne méthode (compatibilité large)
|
|
306
|
+
if (mediaQuery.addEventListener) {
|
|
307
|
+
mediaQuery.addEventListener('change', (e) => {
|
|
308
|
+
if (this.themeMode === 'system') {
|
|
309
|
+
this.applyThemeFromSystem();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
} else {
|
|
313
|
+
// Anciens navigateurs (rare en 2026)
|
|
314
|
+
mediaQuery.addListener((e) => {
|
|
315
|
+
if (this.themeMode === 'system') {
|
|
316
|
+
this.applyThemeFromSystem();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Change le mode thème
|
|
324
|
+
* @param {'light'|'dark'|'system'} mode - Mode à appliquer
|
|
325
|
+
* @param {boolean} [save=true] - Sauvegarder le choix utilisateur ?
|
|
326
|
+
*/
|
|
327
|
+
setThemeMode(mode, save = true) {
|
|
328
|
+
if (!['light', 'dark', 'system'].includes(mode)) {
|
|
329
|
+
console.warn('Mode invalide, valeurs acceptées: light, dark, system');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.themeMode = mode;
|
|
334
|
+
|
|
335
|
+
if (save && mode !== 'system') {
|
|
336
|
+
this.userThemeOverride = mode;
|
|
337
|
+
// Sauvegarde (ex: localStorage ou ton SecureStorage)
|
|
338
|
+
localStorage.setItem('themeOverride', mode);
|
|
339
|
+
} else if (mode === 'system') {
|
|
340
|
+
this.userThemeOverride = null;
|
|
341
|
+
localStorage.removeItem('themeOverride');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this.applyThemeFromSystem();
|
|
345
|
+
}
|
|
346
|
+
|
|
268
347
|
/**
|
|
269
348
|
* Active ou désactive les DevTools
|
|
270
349
|
* @param {boolean} enabled - true pour activer, false pour désactiver
|
|
@@ -431,13 +510,16 @@ class CanvasFramework {
|
|
|
431
510
|
setTheme(theme) {
|
|
432
511
|
this.theme = theme;
|
|
433
512
|
|
|
434
|
-
// Intercepter le context pour remplacer les couleurs globalement
|
|
435
513
|
if (!this.useWebGL) {
|
|
436
514
|
this.wrapContext(this.ctx, theme);
|
|
437
515
|
}
|
|
438
516
|
|
|
439
|
-
//
|
|
440
|
-
|
|
517
|
+
// Protège la boucle
|
|
518
|
+
if (this.components && Array.isArray(this.components)) {
|
|
519
|
+
this.components.forEach(comp => comp.markDirty());
|
|
520
|
+
} else {
|
|
521
|
+
console.warn('[setTheme] components pas encore initialisé');
|
|
522
|
+
}
|
|
441
523
|
}
|
|
442
524
|
|
|
443
525
|
// Switch Theme
|
package/core/UIBuilder.js
CHANGED
|
@@ -55,6 +55,7 @@ import Banner from '../components/Banner.js';
|
|
|
55
55
|
import Chart from '../components/Chart.js';
|
|
56
56
|
import SliverAppBar from '../components/SliverAppBar.js';
|
|
57
57
|
import AudioPlayer from '../components/AudioPlayer.js';
|
|
58
|
+
import Camera from '../components/Camera.js';
|
|
58
59
|
|
|
59
60
|
// Features
|
|
60
61
|
import PullToRefresh from '../features/PullToRefresh.js';
|
|
@@ -96,6 +97,7 @@ const Components = {
|
|
|
96
97
|
AppBar,
|
|
97
98
|
SliverAppBar,
|
|
98
99
|
Chip,
|
|
100
|
+
Camera,
|
|
99
101
|
Stepper,
|
|
100
102
|
Accordion,
|
|
101
103
|
Tabs,
|
package/index.js
CHANGED
|
@@ -62,6 +62,7 @@ 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
64
|
export { default as AudioPlayer } from './components/AudioPlayer.js';
|
|
65
|
+
export { default as Camera } from './components/Camera.js';
|
|
65
66
|
|
|
66
67
|
// Utils
|
|
67
68
|
export { default as SafeArea } from './utils/SafeArea.js';
|
|
@@ -104,7 +105,7 @@ export { default as FeatureFlags } from './manager/FeatureFlags.js';
|
|
|
104
105
|
|
|
105
106
|
// Version du framework
|
|
106
107
|
|
|
107
|
-
export const VERSION = '0.3.
|
|
108
|
+
export const VERSION = '0.3.23';
|
|
108
109
|
|
|
109
110
|
|
|
110
111
|
|