canvasframework 0.6.0 → 0.6.1
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 +315 -368
- package/components/FloatedCamera.js +4 -5
- package/core/CanvasFramework.js +67 -10
- package/package.json +1 -1
package/components/Camera.js
CHANGED
|
@@ -2,7 +2,9 @@ import Component from '../core/Component.js';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Composant Camera autonome avec gestion directe des clics/touches
|
|
5
|
-
* Modes : contain (tout visible + bandes), cover (remplit + crop)
|
|
5
|
+
* Modes : contain (tout visible + bandes), cover (remplit + crop)
|
|
6
|
+
* Layout boutons haut : [switch caméra GAUCHE] [torch CENTRE] [mode DROITE]
|
|
7
|
+
* Layout bouton bas : [capture CENTRE]
|
|
6
8
|
*/
|
|
7
9
|
class Camera extends Component {
|
|
8
10
|
constructor(framework, options = {}) {
|
|
@@ -29,55 +31,56 @@ class Camera extends Component {
|
|
|
29
31
|
|
|
30
32
|
// Feedback capture
|
|
31
33
|
this.flashTimer = null;
|
|
32
|
-
this.previewPhoto = null;
|
|
34
|
+
this.previewPhoto = null;
|
|
35
|
+
this._previewImg = null; // image pré-chargée
|
|
33
36
|
this.previewTimeout = null;
|
|
34
37
|
|
|
35
38
|
this.isStarting = false;
|
|
39
|
+
|
|
40
|
+
// Taille et position des boutons (haut)
|
|
41
|
+
this.topBtnSize = 50;
|
|
42
|
+
this.topBtnMargin = 20;
|
|
36
43
|
}
|
|
37
|
-
|
|
44
|
+
|
|
38
45
|
static cleanupAllCameras() {
|
|
39
46
|
const allVideos = document.querySelectorAll('video');
|
|
40
47
|
allVideos.forEach(video => {
|
|
41
|
-
// Arrêter les streams
|
|
42
48
|
if (video.srcObject) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// Supprimer du DOM
|
|
49
|
+
const stream = video.srcObject;
|
|
50
|
+
if (stream && stream.getTracks) {
|
|
51
|
+
stream.getTracks().forEach(track => track.stop());
|
|
52
|
+
}
|
|
53
|
+
video.srcObject = null;
|
|
54
|
+
}
|
|
51
55
|
if (video.parentNode) {
|
|
52
|
-
|
|
56
|
+
video.parentNode.removeChild(video);
|
|
53
57
|
}
|
|
54
58
|
});
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
async _mount() {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.setupEventListeners();
|
|
62
|
+
super._mount?.();
|
|
63
|
+
|
|
64
|
+
if (this.visible &&
|
|
65
|
+
this.autoStart &&
|
|
66
|
+
!this.stream &&
|
|
67
|
+
!this.isStarting &&
|
|
68
|
+
!this.framework._isNavigating) {
|
|
69
|
+
this.isStarting = true;
|
|
70
|
+
await this.startCamera();
|
|
71
|
+
this.isStarting = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.setupEventListeners();
|
|
72
75
|
}
|
|
73
|
-
|
|
76
|
+
|
|
74
77
|
onUnmount() {
|
|
75
78
|
this.removeEventListeners();
|
|
76
79
|
this.stopCamera();
|
|
77
80
|
if (this.flashTimer) clearTimeout(this.flashTimer);
|
|
78
81
|
if (this.previewTimeout) clearTimeout(this.previewTimeout);
|
|
79
82
|
}
|
|
80
|
-
|
|
83
|
+
|
|
81
84
|
destroy() {
|
|
82
85
|
this.removeEventListeners();
|
|
83
86
|
this.stopCamera();
|
|
@@ -86,35 +89,35 @@ class Camera extends Component {
|
|
|
86
89
|
super.destroy?.();
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
//
|
|
92
|
+
// ─── Événements ─────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
90
94
|
setupEventListeners() {
|
|
91
95
|
this.onTouchStart = this.handleTouchStart.bind(this);
|
|
92
|
-
this.onTouchMove
|
|
93
|
-
this.onTouchEnd
|
|
94
|
-
this.onMouseDown
|
|
95
|
-
this.onMouseMove
|
|
96
|
-
this.onMouseUp
|
|
96
|
+
this.onTouchMove = this.handleTouchMove.bind(this);
|
|
97
|
+
this.onTouchEnd = this.handleTouchEnd.bind(this);
|
|
98
|
+
this.onMouseDown = this.handleMouseDown.bind(this);
|
|
99
|
+
this.onMouseMove = this.handleMouseMove.bind(this);
|
|
100
|
+
this.onMouseUp = this.handleMouseUp.bind(this);
|
|
97
101
|
|
|
98
102
|
const canvas = this.framework.canvas;
|
|
99
103
|
canvas.addEventListener('touchstart', this.onTouchStart, { passive: false });
|
|
100
|
-
canvas.addEventListener('touchmove',
|
|
101
|
-
canvas.addEventListener('touchend',
|
|
102
|
-
canvas.addEventListener('mousedown',
|
|
103
|
-
canvas.addEventListener('mousemove',
|
|
104
|
-
canvas.addEventListener('mouseup',
|
|
104
|
+
canvas.addEventListener('touchmove', this.onTouchMove, { passive: false });
|
|
105
|
+
canvas.addEventListener('touchend', this.onTouchEnd, { passive: false });
|
|
106
|
+
canvas.addEventListener('mousedown', this.onMouseDown);
|
|
107
|
+
canvas.addEventListener('mousemove', this.onMouseMove);
|
|
108
|
+
canvas.addEventListener('mouseup', this.onMouseUp);
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
removeEventListeners() {
|
|
108
112
|
const canvas = this.framework.canvas;
|
|
109
113
|
canvas.removeEventListener('touchstart', this.onTouchStart);
|
|
110
|
-
canvas.removeEventListener('touchmove',
|
|
111
|
-
canvas.removeEventListener('touchend',
|
|
112
|
-
canvas.removeEventListener('mousedown',
|
|
113
|
-
canvas.removeEventListener('mousemove',
|
|
114
|
-
canvas.removeEventListener('mouseup',
|
|
114
|
+
canvas.removeEventListener('touchmove', this.onTouchMove);
|
|
115
|
+
canvas.removeEventListener('touchend', this.onTouchEnd);
|
|
116
|
+
canvas.removeEventListener('mousedown', this.onMouseDown);
|
|
117
|
+
canvas.removeEventListener('mousemove', this.onMouseMove);
|
|
118
|
+
canvas.removeEventListener('mouseup', this.onMouseUp);
|
|
115
119
|
}
|
|
116
120
|
|
|
117
|
-
// Coordonnées locales
|
|
118
121
|
getLocalPos(clientX, clientY) {
|
|
119
122
|
const rect = this.framework.canvas.getBoundingClientRect();
|
|
120
123
|
const globalX = clientX - rect.left;
|
|
@@ -132,13 +135,8 @@ class Camera extends Component {
|
|
|
132
135
|
this.handlePress(pos.x, pos.y);
|
|
133
136
|
}
|
|
134
137
|
|
|
135
|
-
handleTouchMove(e) {
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
handleTouchEnd(e) {
|
|
140
|
-
e.preventDefault();
|
|
141
|
-
}
|
|
138
|
+
handleTouchMove(e) { e.preventDefault(); }
|
|
139
|
+
handleTouchEnd(e) { e.preventDefault(); }
|
|
142
140
|
|
|
143
141
|
handleMouseDown(e) {
|
|
144
142
|
const pos = this.getLocalPos(e.clientX, e.clientY);
|
|
@@ -146,11 +144,12 @@ class Camera extends Component {
|
|
|
146
144
|
}
|
|
147
145
|
|
|
148
146
|
handleMouseMove(e) {}
|
|
147
|
+
handleMouseUp(e) {}
|
|
149
148
|
|
|
150
|
-
|
|
149
|
+
// ─── Caméra ─────────────────────────────────────────────────────────────────
|
|
151
150
|
|
|
152
151
|
async startCamera() {
|
|
153
|
-
|
|
152
|
+
Camera.cleanupAllCameras();
|
|
154
153
|
if (this.stream) return;
|
|
155
154
|
|
|
156
155
|
try {
|
|
@@ -159,8 +158,8 @@ class Camera extends Component {
|
|
|
159
158
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
160
159
|
video: {
|
|
161
160
|
facingMode: this.facingMode,
|
|
162
|
-
width:
|
|
163
|
-
height:
|
|
161
|
+
width: { ideal: 1280 },
|
|
162
|
+
height: { ideal: 720 },
|
|
164
163
|
frameRate: { ideal: 30 }
|
|
165
164
|
}
|
|
166
165
|
});
|
|
@@ -170,16 +169,16 @@ class Camera extends Component {
|
|
|
170
169
|
this.torchSupported = !!caps.torch;
|
|
171
170
|
|
|
172
171
|
this.video = document.createElement('video');
|
|
173
|
-
this.video.autoplay
|
|
172
|
+
this.video.autoplay = true;
|
|
174
173
|
this.video.playsInline = true;
|
|
175
|
-
this.video.muted
|
|
176
|
-
this.video.srcObject
|
|
174
|
+
this.video.muted = true;
|
|
175
|
+
this.video.srcObject = this.stream;
|
|
177
176
|
|
|
178
177
|
this.video.style.position = 'fixed';
|
|
179
|
-
this.video.style.left
|
|
180
|
-
this.video.style.top
|
|
181
|
-
this.video.style.width
|
|
182
|
-
this.video.style.height
|
|
178
|
+
this.video.style.left = '-9999px';
|
|
179
|
+
this.video.style.top = '-9999px';
|
|
180
|
+
this.video.style.width = '1px';
|
|
181
|
+
this.video.style.height = '1px';
|
|
183
182
|
document.body.appendChild(this.video);
|
|
184
183
|
|
|
185
184
|
await new Promise(resolve => {
|
|
@@ -230,9 +229,7 @@ class Camera extends Component {
|
|
|
230
229
|
|
|
231
230
|
const [track] = this.stream.getVideoTracks();
|
|
232
231
|
try {
|
|
233
|
-
await track.applyConstraints({
|
|
234
|
-
advanced: [{ torch: !this.torchOn }]
|
|
235
|
-
});
|
|
232
|
+
await track.applyConstraints({ advanced: [{ torch: !this.torchOn }] });
|
|
236
233
|
this.torchOn = !this.torchOn;
|
|
237
234
|
this.markDirty();
|
|
238
235
|
} catch (err) {
|
|
@@ -247,7 +244,7 @@ class Camera extends Component {
|
|
|
247
244
|
}
|
|
248
245
|
|
|
249
246
|
const canvas = document.createElement('canvas');
|
|
250
|
-
canvas.width
|
|
247
|
+
canvas.width = this.video.videoWidth;
|
|
251
248
|
canvas.height = this.video.videoHeight;
|
|
252
249
|
|
|
253
250
|
const ctx = canvas.getContext('2d');
|
|
@@ -255,7 +252,6 @@ class Camera extends Component {
|
|
|
255
252
|
|
|
256
253
|
const dataUrl = canvas.toDataURL('image/jpeg', 0.92);
|
|
257
254
|
if (this.onPhoto) this.onPhoto(dataUrl);
|
|
258
|
-
console.log('Photo capturée ! DataURL:', dataUrl.substring(0, 50) + '...');
|
|
259
255
|
|
|
260
256
|
// Feedback : flash + preview 3s
|
|
261
257
|
this.flashTimer = setTimeout(() => {
|
|
@@ -263,9 +259,14 @@ class Camera extends Component {
|
|
|
263
259
|
this.markDirty();
|
|
264
260
|
}, 200);
|
|
265
261
|
|
|
262
|
+
// Pré-charger l'image une seule fois
|
|
263
|
+
this._previewImg = new Image();
|
|
264
|
+
this._previewImg.src = dataUrl;
|
|
266
265
|
this.previewPhoto = dataUrl;
|
|
266
|
+
|
|
267
267
|
this.previewTimeout = setTimeout(() => {
|
|
268
268
|
this.previewPhoto = null;
|
|
269
|
+
this._previewImg = null;
|
|
269
270
|
this.markDirty();
|
|
270
271
|
}, 3000);
|
|
271
272
|
|
|
@@ -279,383 +280,329 @@ class Camera extends Component {
|
|
|
279
280
|
this.markDirty();
|
|
280
281
|
}
|
|
281
282
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (relX < 70 && relY < 70) {
|
|
293
|
-
this.switchCamera();
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Torch (haut droite) - zone précise
|
|
298
|
-
if (this.torchSupported && relX > this.width - 75 && relX < this.width - 25 && relY > 20 && relY < 70) {
|
|
299
|
-
this.toggleTorch();
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Switch mode (haut droite, décalé à gauche de la torche)
|
|
304
|
-
const modeButtonX = this.width - 130;
|
|
305
|
-
const modeButtonY = 20;
|
|
306
|
-
if (relX > modeButtonX && relX < modeButtonX + this.modeButtonSize &&
|
|
307
|
-
relY > modeButtonY && relY < modeButtonY + this.modeButtonSize) {
|
|
308
|
-
this.switchFitMode();
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
283
|
+
// ─── Zones des boutons ───────────────────────────────────────────────────────
|
|
284
|
+
// Layout haut : [switch caméra GAUCHE] [torch CENTRE] [mode DROITE]
|
|
285
|
+
// Layout bas : [capture CENTRE]
|
|
286
|
+
|
|
287
|
+
_getSwitchBtnBounds() {
|
|
288
|
+
return {
|
|
289
|
+
x: this.topBtnMargin,
|
|
290
|
+
y: this.topBtnMargin,
|
|
291
|
+
size: this.topBtnSize
|
|
292
|
+
};
|
|
311
293
|
}
|
|
312
294
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
// Rectangle extérieur
|
|
320
|
-
ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
|
|
321
|
-
|
|
322
|
-
// Flèches pointant vers l'intérieur
|
|
323
|
-
const arrowSize = size * 0.15;
|
|
324
|
-
ctx.fillStyle = '#000';
|
|
325
|
-
|
|
326
|
-
// Flèche haut
|
|
327
|
-
ctx.beginPath();
|
|
328
|
-
ctx.moveTo(x + size/2, y + pad - 2);
|
|
329
|
-
ctx.lineTo(x + size/2 - arrowSize, y + pad + arrowSize);
|
|
330
|
-
ctx.lineTo(x + size/2 + arrowSize, y + pad + arrowSize);
|
|
331
|
-
ctx.fill();
|
|
332
|
-
|
|
333
|
-
// Flèche bas
|
|
334
|
-
ctx.beginPath();
|
|
335
|
-
ctx.moveTo(x + size/2, y + size - pad + 2);
|
|
336
|
-
ctx.lineTo(x + size/2 - arrowSize, y + size - pad - arrowSize);
|
|
337
|
-
ctx.lineTo(x + size/2 + arrowSize, y + size - pad - arrowSize);
|
|
338
|
-
ctx.fill();
|
|
339
|
-
|
|
340
|
-
// Flèche gauche
|
|
341
|
-
ctx.beginPath();
|
|
342
|
-
ctx.moveTo(x + pad - 2, y + size/2);
|
|
343
|
-
ctx.lineTo(x + pad + arrowSize, y + size/2 - arrowSize);
|
|
344
|
-
ctx.lineTo(x + pad + arrowSize, y + size/2 + arrowSize);
|
|
345
|
-
ctx.fill();
|
|
346
|
-
|
|
347
|
-
// Flèche droite
|
|
348
|
-
ctx.beginPath();
|
|
349
|
-
ctx.moveTo(x + size - pad + 2, y + size/2);
|
|
350
|
-
ctx.lineTo(x + size - pad - arrowSize, y + size/2 - arrowSize);
|
|
351
|
-
ctx.lineTo(x + size - pad - arrowSize, y + size/2 + arrowSize);
|
|
352
|
-
ctx.fill();
|
|
295
|
+
_getTorchBtnBounds() {
|
|
296
|
+
return {
|
|
297
|
+
x: (this.width - this.topBtnSize) / 2,
|
|
298
|
+
y: this.topBtnMargin,
|
|
299
|
+
size: this.topBtnSize
|
|
300
|
+
};
|
|
353
301
|
}
|
|
354
302
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
303
|
+
_getModeBtnBounds() {
|
|
304
|
+
const yCenter = this.topBtnMargin + this.topBtnSize / 2;
|
|
305
|
+
return {
|
|
306
|
+
x: this.width - this.topBtnMargin - this.modeButtonSize,
|
|
307
|
+
y: yCenter - this.modeButtonSize / 2,
|
|
308
|
+
size: this.modeButtonSize
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
_getCaptureBtnCenter() {
|
|
313
|
+
return {
|
|
314
|
+
cx: this.width / 2,
|
|
315
|
+
cy: this.height - 88
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ─── Gestion des clics ──────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
handlePress(relX, relY) {
|
|
322
|
+
// Guard : ignorer tout clic hors du composant
|
|
323
|
+
if (relX < 0 || relX > this.width || relY < 0 || relY > this.height) return;
|
|
324
|
+
|
|
325
|
+
// Bouton capture (bas centre)
|
|
326
|
+
const { cx, cy } = this._getCaptureBtnCenter();
|
|
327
|
+
if (Math.hypot(relX - cx, relY - cy) < this.captureButtonRadius + 10) {
|
|
328
|
+
this.capturePhoto();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Switch caméra (haut gauche)
|
|
333
|
+
const sw = this._getSwitchBtnBounds();
|
|
334
|
+
if (Math.hypot(relX - (sw.x + sw.size / 2), relY - (sw.y + sw.size / 2)) < sw.size / 2 + 5) {
|
|
335
|
+
this.switchCamera();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Torch (haut centre)
|
|
340
|
+
if (this.torchSupported) {
|
|
341
|
+
const tb = this._getTorchBtnBounds();
|
|
342
|
+
if (Math.hypot(relX - (tb.x + tb.size / 2), relY - (tb.y + tb.size / 2)) < tb.size / 2 + 5) {
|
|
343
|
+
this.toggleTorch();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Mode contain/cover (haut droite)
|
|
349
|
+
const mb = this._getModeBtnBounds();
|
|
350
|
+
if (relX >= mb.x && relX <= mb.x + mb.size && relY >= mb.y && relY <= mb.y + mb.size) {
|
|
351
|
+
this.switchFitMode();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
395
354
|
}
|
|
396
355
|
|
|
397
|
-
|
|
398
|
-
|
|
356
|
+
// ─── Icônes ─────────────────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
drawContainIcon(ctx, x, y, size) {
|
|
399
359
|
const pad = size * 0.2;
|
|
400
360
|
ctx.strokeStyle = '#000';
|
|
401
361
|
ctx.lineWidth = 2;
|
|
402
|
-
|
|
403
|
-
// Rectangle extérieur
|
|
404
362
|
ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
|
|
405
|
-
|
|
406
|
-
// Flèches pointant vers l'intérieur
|
|
363
|
+
|
|
407
364
|
const arrowSize = size * 0.15;
|
|
408
365
|
ctx.fillStyle = '#000';
|
|
409
|
-
|
|
410
|
-
// Flèche haut
|
|
366
|
+
|
|
411
367
|
ctx.beginPath();
|
|
412
|
-
ctx.moveTo(x + size/2, y + pad - 2);
|
|
413
|
-
ctx.lineTo(x + size/2 - arrowSize, y + pad + arrowSize);
|
|
414
|
-
ctx.lineTo(x + size/2 + arrowSize, y + pad + arrowSize);
|
|
368
|
+
ctx.moveTo(x + size / 2, y + pad - 2);
|
|
369
|
+
ctx.lineTo(x + size / 2 - arrowSize, y + pad + arrowSize);
|
|
370
|
+
ctx.lineTo(x + size / 2 + arrowSize, y + pad + arrowSize);
|
|
415
371
|
ctx.fill();
|
|
416
|
-
|
|
417
|
-
// Flèche bas
|
|
372
|
+
|
|
418
373
|
ctx.beginPath();
|
|
419
|
-
ctx.moveTo(x + size/2, y + size - pad + 2);
|
|
420
|
-
ctx.lineTo(x + size/2 - arrowSize, y + size - pad - arrowSize);
|
|
421
|
-
ctx.lineTo(x + size/2 + arrowSize, y + size - pad - arrowSize);
|
|
374
|
+
ctx.moveTo(x + size / 2, y + size - pad + 2);
|
|
375
|
+
ctx.lineTo(x + size / 2 - arrowSize, y + size - pad - arrowSize);
|
|
376
|
+
ctx.lineTo(x + size / 2 + arrowSize, y + size - pad - arrowSize);
|
|
422
377
|
ctx.fill();
|
|
423
|
-
|
|
424
|
-
// Flèche gauche
|
|
378
|
+
|
|
425
379
|
ctx.beginPath();
|
|
426
|
-
ctx.moveTo(x + pad - 2, y + size/2);
|
|
427
|
-
ctx.lineTo(x + pad + arrowSize, y + size/2 - arrowSize);
|
|
428
|
-
ctx.lineTo(x + pad + arrowSize, y + size/2 + arrowSize);
|
|
380
|
+
ctx.moveTo(x + pad - 2, y + size / 2);
|
|
381
|
+
ctx.lineTo(x + pad + arrowSize, y + size / 2 - arrowSize);
|
|
382
|
+
ctx.lineTo(x + pad + arrowSize, y + size / 2 + arrowSize);
|
|
429
383
|
ctx.fill();
|
|
430
|
-
|
|
431
|
-
// Flèche droite
|
|
384
|
+
|
|
432
385
|
ctx.beginPath();
|
|
433
|
-
ctx.moveTo(x + size - pad + 2, y + size/2);
|
|
434
|
-
ctx.lineTo(x + size - pad - arrowSize, y + size/2 - arrowSize);
|
|
435
|
-
ctx.lineTo(x + size - pad - arrowSize, y + size/2 + arrowSize);
|
|
386
|
+
ctx.moveTo(x + size - pad + 2, y + size / 2);
|
|
387
|
+
ctx.lineTo(x + size - pad - arrowSize, y + size / 2 - arrowSize);
|
|
388
|
+
ctx.lineTo(x + size - pad - arrowSize, y + size / 2 + arrowSize);
|
|
436
389
|
ctx.fill();
|
|
437
390
|
}
|
|
438
391
|
|
|
439
392
|
drawCoverIcon(ctx, x, y, size) {
|
|
440
|
-
// Icône "cover" : rectangle avec flèches vers l'extérieur
|
|
441
393
|
const pad = size * 0.2;
|
|
442
394
|
ctx.strokeStyle = '#000';
|
|
443
395
|
ctx.lineWidth = 2;
|
|
444
|
-
|
|
445
|
-
// Rectangle intérieur
|
|
446
396
|
ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
|
|
447
|
-
|
|
448
|
-
// Flèches pointant vers l'extérieur
|
|
397
|
+
|
|
449
398
|
const arrowSize = size * 0.15;
|
|
450
399
|
ctx.fillStyle = '#000';
|
|
451
|
-
|
|
452
|
-
// Flèche haut
|
|
400
|
+
|
|
453
401
|
ctx.beginPath();
|
|
454
|
-
ctx.moveTo(x + size/2, y + 2);
|
|
455
|
-
ctx.lineTo(x + size/2 - arrowSize, y + arrowSize + 2);
|
|
456
|
-
ctx.lineTo(x + size/2 + arrowSize, y + arrowSize + 2);
|
|
402
|
+
ctx.moveTo(x + size / 2, y + 2);
|
|
403
|
+
ctx.lineTo(x + size / 2 - arrowSize, y + arrowSize + 2);
|
|
404
|
+
ctx.lineTo(x + size / 2 + arrowSize, y + arrowSize + 2);
|
|
457
405
|
ctx.fill();
|
|
458
|
-
|
|
459
|
-
// Flèche bas
|
|
406
|
+
|
|
460
407
|
ctx.beginPath();
|
|
461
|
-
ctx.moveTo(x + size/2, y + size - 2);
|
|
462
|
-
ctx.lineTo(x + size/2 - arrowSize, y + size - arrowSize - 2);
|
|
463
|
-
ctx.lineTo(x + size/2 + arrowSize, y + size - arrowSize - 2);
|
|
408
|
+
ctx.moveTo(x + size / 2, y + size - 2);
|
|
409
|
+
ctx.lineTo(x + size / 2 - arrowSize, y + size - arrowSize - 2);
|
|
410
|
+
ctx.lineTo(x + size / 2 + arrowSize, y + size - arrowSize - 2);
|
|
464
411
|
ctx.fill();
|
|
465
|
-
|
|
466
|
-
// Flèche gauche
|
|
412
|
+
|
|
467
413
|
ctx.beginPath();
|
|
468
|
-
ctx.moveTo(x + 2, y + size/2);
|
|
469
|
-
ctx.lineTo(x + arrowSize + 2, y + size/2 - arrowSize);
|
|
470
|
-
ctx.lineTo(x + arrowSize + 2, y + size/2 + arrowSize);
|
|
414
|
+
ctx.moveTo(x + 2, y + size / 2);
|
|
415
|
+
ctx.lineTo(x + arrowSize + 2, y + size / 2 - arrowSize);
|
|
416
|
+
ctx.lineTo(x + arrowSize + 2, y + size / 2 + arrowSize);
|
|
471
417
|
ctx.fill();
|
|
472
|
-
|
|
473
|
-
// Flèche droite
|
|
418
|
+
|
|
474
419
|
ctx.beginPath();
|
|
475
|
-
ctx.moveTo(x + size - 2, y + size/2);
|
|
476
|
-
ctx.lineTo(x + size - arrowSize - 2, y + size/2 - arrowSize);
|
|
477
|
-
ctx.lineTo(x + size - arrowSize - 2, y + size/2 + arrowSize);
|
|
420
|
+
ctx.moveTo(x + size - 2, y + size / 2);
|
|
421
|
+
ctx.lineTo(x + size - arrowSize - 2, y + size / 2 - arrowSize);
|
|
422
|
+
ctx.lineTo(x + size - arrowSize - 2, y + size / 2 + arrowSize);
|
|
478
423
|
ctx.fill();
|
|
479
424
|
}
|
|
480
425
|
|
|
481
426
|
drawSwitchCameraIcon(ctx, x, y, size) {
|
|
482
|
-
// Icône de switch caméra : deux caméras avec flèche circulaire
|
|
483
427
|
const centerX = x + size / 2;
|
|
484
428
|
const centerY = y + size / 2;
|
|
485
|
-
const radius
|
|
486
|
-
|
|
429
|
+
const radius = size * 0.35;
|
|
430
|
+
|
|
487
431
|
ctx.strokeStyle = '#ffffff';
|
|
488
|
-
ctx.lineWidth
|
|
489
|
-
ctx.lineCap
|
|
490
|
-
|
|
491
|
-
// Arc circulaire (flèche de rotation)
|
|
432
|
+
ctx.lineWidth = 3;
|
|
433
|
+
ctx.lineCap = 'round';
|
|
434
|
+
|
|
492
435
|
ctx.beginPath();
|
|
493
436
|
ctx.arc(centerX, centerY, radius, -Math.PI * 0.7, Math.PI * 0.7);
|
|
494
437
|
ctx.stroke();
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const arrowSize = size * 0.15;
|
|
438
|
+
|
|
439
|
+
const arrowSize = size * 0.15;
|
|
498
440
|
const arrowAngle = Math.PI * 0.7;
|
|
499
|
-
const arrowX
|
|
500
|
-
const arrowY
|
|
501
|
-
|
|
441
|
+
const arrowX = centerX + radius * Math.cos(arrowAngle);
|
|
442
|
+
const arrowY = centerY + radius * Math.sin(arrowAngle);
|
|
443
|
+
|
|
502
444
|
ctx.fillStyle = '#ffffff';
|
|
503
445
|
ctx.beginPath();
|
|
504
446
|
ctx.moveTo(arrowX, arrowY);
|
|
505
447
|
ctx.lineTo(arrowX - arrowSize, arrowY - arrowSize * 0.5);
|
|
506
448
|
ctx.lineTo(arrowX - arrowSize * 0.5, arrowY + arrowSize);
|
|
507
449
|
ctx.fill();
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const camWidth = size * 0.25;
|
|
450
|
+
|
|
451
|
+
const camWidth = size * 0.25;
|
|
511
452
|
const camHeight = size * 0.18;
|
|
512
|
-
const camX = centerX - camWidth / 2;
|
|
513
|
-
const camY = centerY - camHeight / 2;
|
|
514
|
-
|
|
515
453
|
ctx.fillStyle = '#ffffff';
|
|
516
|
-
ctx.fillRect(
|
|
517
|
-
|
|
518
|
-
// Objectif
|
|
454
|
+
ctx.fillRect(centerX - camWidth / 2, centerY - camHeight / 2, camWidth, camHeight);
|
|
455
|
+
|
|
519
456
|
ctx.fillStyle = '#000';
|
|
520
457
|
ctx.beginPath();
|
|
521
458
|
ctx.arc(centerX, centerY, size * 0.08, 0, Math.PI * 2);
|
|
522
459
|
ctx.fill();
|
|
523
460
|
}
|
|
524
461
|
|
|
525
|
-
|
|
526
|
-
|
|
462
|
+
drawTorchIcon(ctx, x, y, size, torchOn) {
|
|
463
|
+
const cx = x + size / 2;
|
|
464
|
+
const cy = y + size / 2;
|
|
527
465
|
|
|
528
|
-
|
|
529
|
-
|
|
466
|
+
ctx.fillStyle = torchOn ? 'rgba(255, 235, 59, 0.9)' : 'rgba(0, 0, 0, 0.55)';
|
|
467
|
+
ctx.beginPath();
|
|
468
|
+
ctx.arc(cx, cy, size / 2, 0, Math.PI * 2);
|
|
469
|
+
ctx.fill();
|
|
530
470
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
ctx.
|
|
534
|
-
ctx.
|
|
471
|
+
ctx.strokeStyle = torchOn ? '#f9a825' : 'rgba(255,255,255,0.3)';
|
|
472
|
+
ctx.lineWidth = 1.5;
|
|
473
|
+
ctx.beginPath();
|
|
474
|
+
ctx.arc(cx, cy, size / 2, 0, Math.PI * 2);
|
|
475
|
+
ctx.stroke();
|
|
476
|
+
|
|
477
|
+
ctx.font = `${Math.round(size * 0.48)}px Arial`;
|
|
478
|
+
ctx.textAlign = 'center';
|
|
479
|
+
ctx.textBaseline = 'middle';
|
|
480
|
+
ctx.fillStyle = torchOn ? '#000000' : '#ffffff';
|
|
481
|
+
ctx.fillText('⚡', cx, cy);
|
|
535
482
|
}
|
|
536
483
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
ctx.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
ctx.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
484
|
+
// ─── Rendu ──────────────────────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
draw(ctx) {
|
|
487
|
+
ctx.save();
|
|
488
|
+
|
|
489
|
+
// Fond noir
|
|
490
|
+
ctx.fillStyle = '#000';
|
|
491
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
492
|
+
|
|
493
|
+
// Flash blanc après capture
|
|
494
|
+
if (this.flashTimer) {
|
|
495
|
+
ctx.fillStyle = 'rgba(255,255,255,0.6)';
|
|
496
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// États erreur / chargement / vidéo
|
|
500
|
+
if (this.error) {
|
|
501
|
+
ctx.fillStyle = '#ff4444';
|
|
502
|
+
ctx.font = '16px Arial';
|
|
503
|
+
ctx.textAlign = 'center';
|
|
504
|
+
ctx.textBaseline = 'middle';
|
|
505
|
+
ctx.fillText(this.error, this.x + this.width / 2, this.y + this.height / 2);
|
|
506
|
+
|
|
507
|
+
} else if (!this.loaded) {
|
|
508
|
+
ctx.fillStyle = '#fff';
|
|
509
|
+
ctx.font = '16px Arial';
|
|
510
|
+
ctx.textAlign = 'center';
|
|
511
|
+
ctx.textBaseline = 'middle';
|
|
512
|
+
ctx.fillText('Démarrage caméra...', this.x + this.width / 2, this.y + this.height / 2);
|
|
513
|
+
|
|
514
|
+
} else if (this.video && this.loaded) {
|
|
515
|
+
const videoRatio = this.video.videoWidth / this.video.videoHeight;
|
|
516
|
+
const canvasRatio = this.width / this.height;
|
|
517
|
+
|
|
518
|
+
let drawWidth = this.width;
|
|
519
|
+
let drawHeight = this.height;
|
|
520
|
+
let offsetX = 0;
|
|
521
|
+
let offsetY = 0;
|
|
522
|
+
|
|
523
|
+
if (this.fitMode === 'cover') {
|
|
524
|
+
if (videoRatio > canvasRatio) {
|
|
525
|
+
drawHeight = this.height;
|
|
526
|
+
drawWidth = drawHeight * videoRatio;
|
|
527
|
+
offsetX = (this.width - drawWidth) / 2;
|
|
528
|
+
} else {
|
|
529
|
+
drawWidth = this.width;
|
|
530
|
+
drawHeight = drawWidth / videoRatio;
|
|
531
|
+
offsetY = (this.height - drawHeight) / 2;
|
|
532
|
+
}
|
|
533
|
+
} else if (this.fitMode === 'contain') {
|
|
534
|
+
if (videoRatio > canvasRatio) {
|
|
535
|
+
drawWidth = this.width;
|
|
536
|
+
drawHeight = drawWidth / videoRatio;
|
|
537
|
+
offsetY = (this.height - drawHeight) / 2;
|
|
538
|
+
} else {
|
|
539
|
+
drawHeight = this.height;
|
|
540
|
+
drawWidth = drawHeight * videoRatio;
|
|
541
|
+
offsetX = (this.width - drawWidth) / 2;
|
|
542
|
+
}
|
|
567
543
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
544
|
+
|
|
545
|
+
ctx.drawImage(this.video, this.x + offsetX, this.y + offsetY, drawWidth, drawHeight);
|
|
546
|
+
|
|
547
|
+
// Mini preview dernière photo (bas droite, 3s)
|
|
548
|
+
if (this.previewPhoto && this._previewImg?.complete) {
|
|
549
|
+
const previewSize = 80;
|
|
550
|
+
const px = this.x + this.width - previewSize - 10;
|
|
551
|
+
const py = this.y + this.height - previewSize - 10;
|
|
552
|
+
ctx.drawImage(this._previewImg, px, py, previewSize, previewSize);
|
|
553
|
+
ctx.strokeStyle = '#fff';
|
|
554
|
+
ctx.lineWidth = 2;
|
|
555
|
+
ctx.strokeRect(px, py, previewSize, previewSize);
|
|
577
556
|
}
|
|
578
557
|
}
|
|
579
558
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (this.previewPhoto) {
|
|
584
|
-
const previewSize = 80;
|
|
585
|
-
const img = new Image();
|
|
586
|
-
img.src = this.previewPhoto;
|
|
587
|
-
ctx.drawImage(img, this.x + this.width - previewSize - 10, this.y + this.height - previewSize - 10, previewSize, previewSize);
|
|
588
|
-
ctx.strokeStyle = '#fff';
|
|
589
|
-
ctx.lineWidth = 2;
|
|
590
|
-
ctx.strokeRect(this.x + this.width - previewSize - 10, this.y + this.height - previewSize - 10, previewSize, previewSize);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
559
|
+
// ── Barre bas ───────────────────────────────────────────────────────────
|
|
560
|
+
ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
|
561
|
+
ctx.fillRect(this.x, this.y + this.height - 100, this.width, 100);
|
|
593
562
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
// Bouton capture
|
|
599
|
-
ctx.fillStyle = '#ffffff';
|
|
600
|
-
ctx.beginPath();
|
|
601
|
-
ctx.arc(this.x + this.width/2, this.y + this.height - 50, this.captureButtonRadius, 0, Math.PI * 2);
|
|
602
|
-
ctx.fill();
|
|
603
|
-
|
|
604
|
-
ctx.strokeStyle = '#ff4444';
|
|
605
|
-
ctx.lineWidth = 6;
|
|
606
|
-
ctx.beginPath();
|
|
607
|
-
ctx.arc(this.x + this.width/2, this.y + this.height - 50, this.captureButtonRadius + 10, 0, Math.PI * 2);
|
|
608
|
-
ctx.stroke();
|
|
609
|
-
|
|
610
|
-
// Switch caméra avec icône
|
|
611
|
-
const switchBtnX = this.x + 20;
|
|
612
|
-
const switchBtnY = this.y + 20;
|
|
613
|
-
const switchBtnSize = 50;
|
|
614
|
-
|
|
615
|
-
ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
|
616
|
-
ctx.beginPath();
|
|
617
|
-
ctx.arc(switchBtnX + switchBtnSize/2, switchBtnY + switchBtnSize/2, switchBtnSize/2, 0, Math.PI * 2);
|
|
618
|
-
ctx.fill();
|
|
619
|
-
|
|
620
|
-
this.drawSwitchCameraIcon(ctx, switchBtnX, switchBtnY, switchBtnSize);
|
|
621
|
-
|
|
622
|
-
// Bouton torche (haut droite)
|
|
623
|
-
if (this.torchSupported) {
|
|
624
|
-
const torchBtnX = this.x + this.width - 75;
|
|
625
|
-
const torchBtnY = this.y + 20;
|
|
626
|
-
const torchBtnSize = 50;
|
|
627
|
-
|
|
628
|
-
ctx.fillStyle = this.torchOn ? 'rgba(255,235,59,0.8)' : 'rgba(0,0,0,0.5)';
|
|
563
|
+
// Bouton capture (bas centre)
|
|
564
|
+
const { cx, cy } = this._getCaptureBtnCenter();
|
|
565
|
+
ctx.fillStyle = '#ffffff';
|
|
629
566
|
ctx.beginPath();
|
|
630
|
-
ctx.arc(
|
|
567
|
+
ctx.arc(this.x + cx, this.y + cy, this.captureButtonRadius, 0, Math.PI * 2);
|
|
631
568
|
ctx.fill();
|
|
632
569
|
|
|
633
|
-
ctx.
|
|
634
|
-
ctx.
|
|
635
|
-
ctx.
|
|
636
|
-
ctx.
|
|
637
|
-
ctx.
|
|
638
|
-
}
|
|
570
|
+
ctx.strokeStyle = '#ff4444';
|
|
571
|
+
ctx.lineWidth = 6;
|
|
572
|
+
ctx.beginPath();
|
|
573
|
+
ctx.arc(this.x + cx, this.y + cy, this.captureButtonRadius + 10, 0, Math.PI * 2);
|
|
574
|
+
ctx.stroke();
|
|
639
575
|
|
|
640
|
-
|
|
641
|
-
const btnX = this.x + this.width - 130;
|
|
642
|
-
const btnY = this.y + 20;
|
|
576
|
+
// ── Boutons haut ────────────────────────────────────────────────────────
|
|
643
577
|
|
|
644
|
-
|
|
645
|
-
|
|
578
|
+
// 1. Switch caméra (GAUCHE)
|
|
579
|
+
const sw = this._getSwitchBtnBounds();
|
|
580
|
+
ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
|
581
|
+
ctx.beginPath();
|
|
582
|
+
ctx.arc(this.x + sw.x + sw.size / 2, this.y + sw.y + sw.size / 2, sw.size / 2, 0, Math.PI * 2);
|
|
583
|
+
ctx.fill();
|
|
584
|
+
this.drawSwitchCameraIcon(ctx, this.x + sw.x, this.y + sw.y, sw.size);
|
|
646
585
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
586
|
+
// 2. Mode contain/cover (DROITE)
|
|
587
|
+
const mb = this._getModeBtnBounds();
|
|
588
|
+
ctx.fillStyle = 'rgba(255,255,255,0.9)';
|
|
589
|
+
ctx.beginPath();
|
|
590
|
+
ctx.arc(this.x + mb.x + mb.size / 2, this.y + mb.y + mb.size / 2, mb.size / 2, 0, Math.PI * 2);
|
|
591
|
+
ctx.fill();
|
|
592
|
+
if (this.fitMode === 'contain') {
|
|
593
|
+
this.drawContainIcon(ctx, this.x + mb.x, this.y + mb.y, mb.size);
|
|
594
|
+
} else {
|
|
595
|
+
this.drawCoverIcon(ctx, this.x + mb.x, this.y + mb.y, mb.size);
|
|
596
|
+
}
|
|
650
597
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
598
|
+
// 3. Torch (CENTRE) — dessiné en dernier, jamais caché
|
|
599
|
+
if (this.torchSupported) {
|
|
600
|
+
const tb = this._getTorchBtnBounds();
|
|
601
|
+
this.drawTorchIcon(ctx, this.x + tb.x, this.y + tb.y, tb.size, this.torchOn);
|
|
602
|
+
}
|
|
656
603
|
|
|
657
|
-
|
|
658
|
-
}
|
|
604
|
+
ctx.restore();
|
|
605
|
+
}
|
|
659
606
|
}
|
|
660
607
|
|
|
661
608
|
export default Camera;
|
|
@@ -572,11 +572,10 @@ class FloatedCamera extends Component {
|
|
|
572
572
|
|
|
573
573
|
// 2. Mode contain/cover (DROITE)
|
|
574
574
|
const mb = this._getModeBtnBounds();
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
ctx.strokeRect(this.x + mb.x, this.y + mb.y, mb.size, mb.size);
|
|
575
|
+
ctx.fillStyle = 'rgba(255,255,255,0.9)';
|
|
576
|
+
ctx.beginPath();
|
|
577
|
+
ctx.arc(this.x + mb.x + mb.size / 2, this.y + mb.y + mb.size / 2, mb.size / 2, 0, Math.PI * 2);
|
|
578
|
+
ctx.fill();
|
|
580
579
|
if (this.fitMode === 'contain') {
|
|
581
580
|
this.drawContainIcon(ctx, this.x + mb.x, this.y + mb.y, mb.size);
|
|
582
581
|
} else {
|
package/core/CanvasFramework.js
CHANGED
|
@@ -585,6 +585,7 @@ class CanvasFramework {
|
|
|
585
585
|
|
|
586
586
|
this.setupEventListeners();
|
|
587
587
|
this.setupHistoryListener();
|
|
588
|
+
this._initNavigationGuard(); // ← AJOUTER
|
|
588
589
|
|
|
589
590
|
this.startRenderLoop();
|
|
590
591
|
|
|
@@ -635,6 +636,13 @@ class CanvasFramework {
|
|
|
635
636
|
}
|
|
636
637
|
|
|
637
638
|
}
|
|
639
|
+
|
|
640
|
+
_initNavigationGuard() {
|
|
641
|
+
// Injecter 2 entrées : une base + la route actuelle
|
|
642
|
+
// Ainsi le navigateur a toujours une entrée "avant" à consommer
|
|
643
|
+
window.history.replaceState({ route: '/', _guard: true }, '', '/');
|
|
644
|
+
window.history.pushState({ route: '/', _app: true }, '', '/');
|
|
645
|
+
}
|
|
638
646
|
|
|
639
647
|
/**
|
|
640
648
|
* Crée un élément DOM temporaire, l'ajoute au body, exécute une callback, puis le supprime
|
|
@@ -1963,16 +1971,51 @@ class CanvasFramework {
|
|
|
1963
1971
|
* @private
|
|
1964
1972
|
*/
|
|
1965
1973
|
setupHistoryListener() {
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1974
|
+
window.addEventListener('popstate', (e) => {
|
|
1975
|
+
// Cas 1 : L'app a un historique interne → naviguer en arrière dans l'app
|
|
1976
|
+
if (this.historyIndex > 0) {
|
|
1977
|
+
this.historyIndex--;
|
|
1978
|
+
const entry = this.history[this.historyIndex];
|
|
1979
|
+
|
|
1980
|
+
// Recharger la vue précédente sans toucher window.history
|
|
1981
|
+
this._navigateInternal(entry.path, {
|
|
1982
|
+
replace: true,
|
|
1983
|
+
animate: true,
|
|
1984
|
+
direction: 'back'
|
|
1985
|
+
});
|
|
1986
|
+
|
|
1987
|
+
// Réinjecter une entrée pour ne jamais vider la pile navigateur
|
|
1988
|
+
window.history.pushState({ route: entry.path, _app: true }, '', entry.path);
|
|
1989
|
+
|
|
1990
|
+
} else {
|
|
1991
|
+
// Cas 2 : Plus d'historique interne → réinjecter et ignorer
|
|
1992
|
+
// (empêche de quitter l'app)
|
|
1993
|
+
const currentPath = this.currentRoute || '/';
|
|
1994
|
+
window.history.pushState({ route: currentPath, _app: true }, '', currentPath);
|
|
1995
|
+
|
|
1996
|
+
// Optionnel : afficher un toast "Appuyez encore pour quitter"
|
|
1997
|
+
this._handleBackExitAttempt();
|
|
1998
|
+
}
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
_handleBackExitAttempt() {
|
|
2003
|
+
const now = Date.now();
|
|
2004
|
+
|
|
2005
|
+
if (this._lastBackAttempt && (now - this._lastBackAttempt) < 2000) {
|
|
2006
|
+
// Deux back rapides → vraiment quitter (sur PWA/WebView)
|
|
2007
|
+
// Sur navigateur standard ce n'est pas possible, mais sur Capacitor/Cordova oui
|
|
2008
|
+
if (window.navigator.app?.exitApp) {
|
|
2009
|
+
window.navigator.app.exitApp(); // Cordova
|
|
2010
|
+
} else if (window.Capacitor?.Plugins?.App) {
|
|
2011
|
+
window.Capacitor.Plugins.App.exitApp(); // Capacitor
|
|
2012
|
+
}
|
|
2013
|
+
this._lastBackAttempt = null;
|
|
2014
|
+
} else {
|
|
2015
|
+
this._lastBackAttempt = now;
|
|
2016
|
+
this.showToast('Appuyez encore pour quitter', 2000);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
1976
2019
|
|
|
1977
2020
|
// ===== MÉTHODES DE ROUTING =====
|
|
1978
2021
|
|
|
@@ -2207,6 +2250,20 @@ class CanvasFramework {
|
|
|
2207
2250
|
}
|
|
2208
2251
|
|
|
2209
2252
|
this._maxScrollDirty = true;
|
|
2253
|
+
|
|
2254
|
+
// Historique navigateur : seulement si ce n'est pas un retour interne
|
|
2255
|
+
if (!options._internal) {
|
|
2256
|
+
if (!replace) {
|
|
2257
|
+
window.history.pushState({ route: path, _app: true }, '', path);
|
|
2258
|
+
} else {
|
|
2259
|
+
window.history.replaceState({ route: path, _app: true }, '', path);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// Alias pour usage interne (sans toucher window.history)
|
|
2265
|
+
_navigateInternal(path, options = {}) {
|
|
2266
|
+
return this.navigateTo(path, { ...options, _internal: true });
|
|
2210
2267
|
}
|
|
2211
2268
|
|
|
2212
2269
|
/**
|