canvasframework 0.3.6
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/README.md +554 -0
- package/components/Accordion.js +252 -0
- package/components/AndroidDatePickerDialog.js +398 -0
- package/components/AppBar.js +225 -0
- package/components/Avatar.js +202 -0
- package/components/BottomNavigationBar.js +205 -0
- package/components/BottomSheet.js +374 -0
- package/components/Button.js +225 -0
- package/components/Card.js +193 -0
- package/components/Checkbox.js +180 -0
- package/components/Chip.js +212 -0
- package/components/CircularProgress.js +143 -0
- package/components/ContextMenu.js +116 -0
- package/components/DatePicker.js +257 -0
- package/components/Dialog.js +367 -0
- package/components/Divider.js +125 -0
- package/components/Drawer.js +261 -0
- package/components/FAB.js +270 -0
- package/components/FileUpload.js +315 -0
- package/components/IOSDatePickerWheel.js +268 -0
- package/components/ImageCarousel.js +193 -0
- package/components/ImageComponent.js +223 -0
- package/components/Input.js +309 -0
- package/components/List.js +94 -0
- package/components/ListItem.js +223 -0
- package/components/Modal.js +364 -0
- package/components/MultiSelectDialog.js +206 -0
- package/components/NumberInput.js +271 -0
- package/components/ProgressBar.js +88 -0
- package/components/RadioButton.js +142 -0
- package/components/SearchInput.js +315 -0
- package/components/SegmentedControl.js +202 -0
- package/components/Select.js +199 -0
- package/components/SelectDialog.js +255 -0
- package/components/Slider.js +113 -0
- package/components/Snackbar.js +243 -0
- package/components/Stepper.js +281 -0
- package/components/SwipeableListItem.js +179 -0
- package/components/Switch.js +147 -0
- package/components/Table.js +492 -0
- package/components/Tabs.js +125 -0
- package/components/Text.js +141 -0
- package/components/TextField.js +331 -0
- package/components/Toast.js +236 -0
- package/components/TreeView.js +420 -0
- package/components/Video.js +397 -0
- package/components/View.js +140 -0
- package/components/VirtualList.js +120 -0
- package/core/CanvasFramework.js +1271 -0
- package/core/CanvasWork.js +32 -0
- package/core/Component.js +153 -0
- package/core/LogicWorker.js +25 -0
- package/core/WebGLCanvasAdapter.js +1369 -0
- package/features/Column.js +43 -0
- package/features/Grid.js +47 -0
- package/features/LayoutComponent.js +43 -0
- package/features/OpenStreetMap.js +310 -0
- package/features/Positioned.js +33 -0
- package/features/PullToRefresh.js +328 -0
- package/features/Row.js +40 -0
- package/features/SignaturePad.js +257 -0
- package/features/Skeleton.js +84 -0
- package/features/Stack.js +21 -0
- package/index.js +101 -0
- package/manager/AccessibilityManager.js +107 -0
- package/manager/ErrorHandler.js +59 -0
- package/manager/FeatureFlags.js +60 -0
- package/manager/MemoryManager.js +107 -0
- package/manager/PerformanceMonitor.js +84 -0
- package/manager/SecurityManager.js +54 -0
- package/package.json +28 -0
- package/utils/AnimationEngine.js +428 -0
- package/utils/DataStore.js +403 -0
- package/utils/EventBus.js +407 -0
- package/utils/FetchClient.js +74 -0
- package/utils/FormValidator.js +355 -0
- package/utils/GeoLocationService.js +62 -0
- package/utils/I18n.js +207 -0
- package/utils/IndexedDBManager.js +273 -0
- package/utils/OfflineSyncManager.js +342 -0
- package/utils/QueryBuilder.js +478 -0
- package/utils/SafeArea.js +64 -0
- package/utils/SecureStorage.js +289 -0
- package/utils/StateManager.js +207 -0
- package/utils/WebSocketClient.js +66 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Lecteur vidéo
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @property {string} src - URL de la vidéo
|
|
7
|
+
* @property {string} poster - URL de l'image d'affiche
|
|
8
|
+
* @property {boolean} playing - En cours de lecture
|
|
9
|
+
* @property {string} platform - Plateforme
|
|
10
|
+
* @property {boolean} showControls - Afficher les contrôles
|
|
11
|
+
* @property {number|null} controlsTimeout - Timeout des contrôles
|
|
12
|
+
* @property {number} currentTime - Temps actuel
|
|
13
|
+
* @property {number} duration - Durée totale
|
|
14
|
+
* @property {number} progress - Progression (0-100)
|
|
15
|
+
* @property {number} volume - Volume (0-1)
|
|
16
|
+
* @property {boolean} showVolume - Afficher le contrôle de volume
|
|
17
|
+
* @property {boolean} fullscreen - Plein écran
|
|
18
|
+
* @property {boolean} loaded - Vidéo chargée
|
|
19
|
+
* @property {HTMLVideoElement} videoElement - Élément vidéo HTML
|
|
20
|
+
* @property {number} controlsHeight - Hauteur des contrôles
|
|
21
|
+
* @property {number} volumeHeight - Hauteur du contrôle de volume
|
|
22
|
+
* @property {Function} onPlay - Callback à la lecture
|
|
23
|
+
* @property {Function} onPause - Callback à la pause
|
|
24
|
+
* @property {Function} onEnded - Callback à la fin
|
|
25
|
+
* @property {Function} onFullscreen - Callback au plein écran
|
|
26
|
+
*/
|
|
27
|
+
class Video extends Component {
|
|
28
|
+
/**
|
|
29
|
+
* Crée une instance de Video
|
|
30
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
31
|
+
* @param {Object} [options={}] - Options de configuration
|
|
32
|
+
* @param {string} [options.src=''] - URL de la vidéo
|
|
33
|
+
* @param {string} [options.poster=''] - URL de l'image d'affiche
|
|
34
|
+
* @param {boolean} [options.playing=false] - Lecture initiale
|
|
35
|
+
* @param {boolean} [options.showControls=true] - Afficher les contrôles
|
|
36
|
+
* @param {Function} [options.onPlay] - Callback à la lecture
|
|
37
|
+
* @param {Function} [options.onPause] - Callback à la pause
|
|
38
|
+
* @param {Function} [options.onEnded] - Callback à la fin
|
|
39
|
+
* @param {Function} [options.onFullscreen] - Callback au plein écran
|
|
40
|
+
*/
|
|
41
|
+
constructor(framework, options = {}) {
|
|
42
|
+
super(framework, options);
|
|
43
|
+
this.src = options.src || '';
|
|
44
|
+
this.poster = options.poster || '';
|
|
45
|
+
this.playing = false;
|
|
46
|
+
this.platform = framework.platform;
|
|
47
|
+
this.showControls = true;
|
|
48
|
+
this.controlsTimeout = null;
|
|
49
|
+
this.currentTime = 0;
|
|
50
|
+
this.duration = 0;
|
|
51
|
+
this.progress = 0;
|
|
52
|
+
this.volume = 1;
|
|
53
|
+
this.showVolume = false;
|
|
54
|
+
this.fullscreen = false;
|
|
55
|
+
this.loaded = false;
|
|
56
|
+
|
|
57
|
+
// Éléments de contrôle
|
|
58
|
+
this.controlsHeight = 50;
|
|
59
|
+
this.volumeHeight = 100;
|
|
60
|
+
|
|
61
|
+
// Créer l'élément vidéo HTML5
|
|
62
|
+
this.videoElement = document.createElement('video');
|
|
63
|
+
this.videoElement.style.position = 'fixed';
|
|
64
|
+
this.videoElement.style.left = '-9999px'; // Caché
|
|
65
|
+
this.videoElement.style.top = '-9999px';
|
|
66
|
+
this.videoElement.style.width = '0';
|
|
67
|
+
this.videoElement.style.height = '0';
|
|
68
|
+
this.videoElement.src = this.src;
|
|
69
|
+
this.videoElement.poster = this.poster;
|
|
70
|
+
this.videoElement.preload = 'auto';
|
|
71
|
+
this.videoElement.crossOrigin = 'anonymous'; // Important pour les vidéos externes
|
|
72
|
+
this.videoElement.controls = false; // Nous gérons nos propres contrôles
|
|
73
|
+
|
|
74
|
+
document.body.appendChild(this.videoElement);
|
|
75
|
+
|
|
76
|
+
// Événements de la vidéo
|
|
77
|
+
this.videoElement.addEventListener('loadedmetadata', () => {
|
|
78
|
+
this.duration = this.videoElement.duration;
|
|
79
|
+
this.loaded = true;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.videoElement.addEventListener('timeupdate', () => {
|
|
83
|
+
this.currentTime = this.videoElement.currentTime;
|
|
84
|
+
this.progress = (this.currentTime / this.duration) * 100;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this.videoElement.addEventListener('ended', () => {
|
|
88
|
+
this.playing = false;
|
|
89
|
+
if (this.onEnded) this.onEnded();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.videoElement.addEventListener('play', () => {
|
|
93
|
+
this.playing = true;
|
|
94
|
+
if (this.onPlay) this.onPlay();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this.videoElement.addEventListener('pause', () => {
|
|
98
|
+
this.playing = false;
|
|
99
|
+
if (this.onPause) this.onPause();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Événements
|
|
103
|
+
this.onPlay = options.onPlay;
|
|
104
|
+
this.onPause = options.onPause;
|
|
105
|
+
this.onEnded = options.onEnded;
|
|
106
|
+
this.onFullscreen = options.onFullscreen;
|
|
107
|
+
|
|
108
|
+
// Définir onPress pour les contrôles
|
|
109
|
+
this.onPress = this.handlePress.bind(this);
|
|
110
|
+
this.onMove = this.handleMove.bind(this);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Démarre la lecture
|
|
115
|
+
*/
|
|
116
|
+
play() {
|
|
117
|
+
if (this.videoElement) {
|
|
118
|
+
this.videoElement.play();
|
|
119
|
+
this.playing = true;
|
|
120
|
+
this.showControlsTemporarily();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Met en pause
|
|
126
|
+
*/
|
|
127
|
+
pause() {
|
|
128
|
+
if (this.videoElement) {
|
|
129
|
+
this.videoElement.pause();
|
|
130
|
+
this.playing = false;
|
|
131
|
+
this.showControlsTemporarily();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Alterne lecture/pause
|
|
137
|
+
*/
|
|
138
|
+
togglePlay() {
|
|
139
|
+
if (this.playing) {
|
|
140
|
+
this.pause();
|
|
141
|
+
} else {
|
|
142
|
+
this.play();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Affiche temporairement les contrôles
|
|
148
|
+
* @private
|
|
149
|
+
*/
|
|
150
|
+
showControlsTemporarily() {
|
|
151
|
+
this.showControls = true;
|
|
152
|
+
clearTimeout(this.controlsTimeout);
|
|
153
|
+
this.controlsTimeout = setTimeout(() => {
|
|
154
|
+
if (this.playing) {
|
|
155
|
+
this.showControls = false;
|
|
156
|
+
}
|
|
157
|
+
}, 3000);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Gère la pression (clic)
|
|
162
|
+
* @param {number} x - Coordonnée X
|
|
163
|
+
* @param {number} y - Coordonnée Y
|
|
164
|
+
* @private
|
|
165
|
+
*/
|
|
166
|
+
handlePress(x, y) {
|
|
167
|
+
const adjustedY = y - this.framework.scrollOffset;
|
|
168
|
+
|
|
169
|
+
// Montrer les contrôles
|
|
170
|
+
this.showControls = true;
|
|
171
|
+
this.showControlsTemporarily();
|
|
172
|
+
|
|
173
|
+
// Bouton play/pause central
|
|
174
|
+
const centerX = this.x + this.width / 2;
|
|
175
|
+
const centerY = this.y + this.height / 2;
|
|
176
|
+
|
|
177
|
+
if (x >= centerX - 30 && x <= centerX + 30 &&
|
|
178
|
+
adjustedY >= centerY - 30 && adjustedY <= centerY + 30) {
|
|
179
|
+
this.togglePlay();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Barre de progression
|
|
184
|
+
if (this.showControls && this.loaded) {
|
|
185
|
+
const progressBarY = this.y + this.height - 30;
|
|
186
|
+
if (adjustedY >= progressBarY && adjustedY <= progressBarY + 10) {
|
|
187
|
+
const clickX = x - this.x;
|
|
188
|
+
this.progress = (clickX / this.width) * 100;
|
|
189
|
+
this.currentTime = (this.duration * this.progress) / 100;
|
|
190
|
+
if (this.videoElement) {
|
|
191
|
+
this.videoElement.currentTime = this.currentTime;
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Bouton plein écran
|
|
198
|
+
const fullscreenX = this.x + this.width - 40;
|
|
199
|
+
const fullscreenY = this.y + this.height - 40;
|
|
200
|
+
|
|
201
|
+
if (x >= fullscreenX && x <= fullscreenX + 30 &&
|
|
202
|
+
adjustedY >= fullscreenY && adjustedY <= fullscreenY + 30) {
|
|
203
|
+
this.toggleFullscreen();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Gère le mouvement
|
|
210
|
+
* @param {number} x - Coordonnée X
|
|
211
|
+
* @param {number} y - Coordonnée Y
|
|
212
|
+
* @private
|
|
213
|
+
*/
|
|
214
|
+
handleMove(x, y) {
|
|
215
|
+
// Pour des interactions supplémentaires
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Alterne le plein écran
|
|
220
|
+
*/
|
|
221
|
+
toggleFullscreen() {
|
|
222
|
+
if (!document.fullscreenElement && this.videoElement.requestFullscreen) {
|
|
223
|
+
this.videoElement.requestFullscreen();
|
|
224
|
+
this.fullscreen = true;
|
|
225
|
+
if (this.onFullscreen) this.onFullscreen(true);
|
|
226
|
+
} else if (document.exitFullscreen) {
|
|
227
|
+
document.exitFullscreen();
|
|
228
|
+
this.fullscreen = false;
|
|
229
|
+
if (this.onFullscreen) this.onFullscreen(false);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Dessine le lecteur vidéo
|
|
235
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
236
|
+
*/
|
|
237
|
+
draw(ctx) {
|
|
238
|
+
ctx.save();
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
// Dessiner la vidéo sur le canvas
|
|
242
|
+
if (this.loaded && this.videoElement.readyState >= 2) {
|
|
243
|
+
ctx.drawImage(this.videoElement, this.x, this.y, this.width, this.height);
|
|
244
|
+
} else {
|
|
245
|
+
// Affichage de chargement
|
|
246
|
+
ctx.fillStyle = '#000000';
|
|
247
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
248
|
+
|
|
249
|
+
ctx.fillStyle = '#FFFFFF';
|
|
250
|
+
ctx.font = '16px Arial';
|
|
251
|
+
ctx.textAlign = 'center';
|
|
252
|
+
ctx.textBaseline = 'middle';
|
|
253
|
+
ctx.fillText('Chargement de la vidéo...',
|
|
254
|
+
this.x + this.width/2, this.y + this.height/2);
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
// En cas d'erreur CORS ou autre
|
|
258
|
+
ctx.fillStyle = '#000000';
|
|
259
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
260
|
+
|
|
261
|
+
ctx.fillStyle = '#FFFFFF';
|
|
262
|
+
ctx.font = '14px Arial';
|
|
263
|
+
ctx.textAlign = 'center';
|
|
264
|
+
ctx.textBaseline = 'middle';
|
|
265
|
+
ctx.fillText('Vidéo: ' + (this.src.substring(0, 30) + '...'),
|
|
266
|
+
this.x + this.width/2, this.y + this.height/2 - 10);
|
|
267
|
+
ctx.fillText('Cliquez pour lire',
|
|
268
|
+
this.x + this.width/2, this.y + this.height/2 + 10);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Overlay sombre quand en pause
|
|
272
|
+
if (!this.playing || this.showControls) {
|
|
273
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
|
274
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Bouton play/pause au centre (quand pause ou contrôles visibles)
|
|
278
|
+
if (!this.playing || this.showControls) {
|
|
279
|
+
const centerX = this.x + this.width / 2;
|
|
280
|
+
const centerY = this.y + this.height / 2;
|
|
281
|
+
|
|
282
|
+
// Fond rond pour le bouton
|
|
283
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
284
|
+
ctx.beginPath();
|
|
285
|
+
ctx.arc(centerX, centerY, 30, 0, Math.PI * 2);
|
|
286
|
+
ctx.fill();
|
|
287
|
+
|
|
288
|
+
// Icône play/pause
|
|
289
|
+
ctx.fillStyle = '#FFFFFF';
|
|
290
|
+
if (this.playing) {
|
|
291
|
+
// Icône pause
|
|
292
|
+
ctx.fillRect(centerX - 8, centerY - 15, 6, 30);
|
|
293
|
+
ctx.fillRect(centerX + 2, centerY - 15, 6, 30);
|
|
294
|
+
} else {
|
|
295
|
+
// Icône play (triangle)
|
|
296
|
+
ctx.beginPath();
|
|
297
|
+
ctx.moveTo(centerX - 5, centerY - 15);
|
|
298
|
+
ctx.lineTo(centerX - 5, centerY + 15);
|
|
299
|
+
ctx.lineTo(centerX + 20, centerY);
|
|
300
|
+
ctx.closePath();
|
|
301
|
+
ctx.fill();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Contrôles en bas
|
|
306
|
+
if (this.showControls && this.loaded) {
|
|
307
|
+
// Overlay pour les contrôles
|
|
308
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
|
309
|
+
ctx.fillRect(this.x, this.y + this.height - this.controlsHeight,
|
|
310
|
+
this.width, this.controlsHeight);
|
|
311
|
+
|
|
312
|
+
// Barre de progression
|
|
313
|
+
const progressX = this.x + 10;
|
|
314
|
+
const progressY = this.y + this.height - 25;
|
|
315
|
+
const progressWidth = this.width - 60; // Réduit pour laisser place au bouton plein écran
|
|
316
|
+
|
|
317
|
+
// Fond de la barre
|
|
318
|
+
ctx.fillStyle = '#555555';
|
|
319
|
+
ctx.fillRect(progressX, progressY, progressWidth, 4);
|
|
320
|
+
|
|
321
|
+
// Progression actuelle
|
|
322
|
+
ctx.fillStyle = '#FF0000';
|
|
323
|
+
const currentProgressWidth = (progressWidth * this.progress) / 100;
|
|
324
|
+
ctx.fillRect(progressX, progressY, currentProgressWidth, 4);
|
|
325
|
+
|
|
326
|
+
// Curseur de progression
|
|
327
|
+
const thumbX = progressX + currentProgressWidth;
|
|
328
|
+
ctx.fillStyle = '#FFFFFF';
|
|
329
|
+
ctx.beginPath();
|
|
330
|
+
ctx.arc(thumbX, progressY + 2, 6, 0, Math.PI * 2);
|
|
331
|
+
ctx.fill();
|
|
332
|
+
|
|
333
|
+
// Temps
|
|
334
|
+
const currentTimeStr = this.formatTime(this.currentTime);
|
|
335
|
+
const totalTimeStr = this.formatTime(this.duration);
|
|
336
|
+
|
|
337
|
+
ctx.fillStyle = '#FFFFFF';
|
|
338
|
+
ctx.font = '12px Arial';
|
|
339
|
+
ctx.textAlign = 'left';
|
|
340
|
+
ctx.fillText(currentTimeStr, this.x + 10, this.y + this.height - 35);
|
|
341
|
+
|
|
342
|
+
ctx.textAlign = 'right';
|
|
343
|
+
ctx.fillText(totalTimeStr, this.x + progressWidth + 10, this.y + this.height - 35);
|
|
344
|
+
|
|
345
|
+
// Bouton plein écran
|
|
346
|
+
const fullscreenX = this.x + this.width - 40;
|
|
347
|
+
const fullscreenY = this.y + this.height - 40;
|
|
348
|
+
|
|
349
|
+
ctx.strokeStyle = '#FFFFFF';
|
|
350
|
+
ctx.lineWidth = 2;
|
|
351
|
+
ctx.strokeRect(fullscreenX, fullscreenY, 20, 20);
|
|
352
|
+
ctx.beginPath();
|
|
353
|
+
ctx.moveTo(fullscreenX + 5, fullscreenY + 5);
|
|
354
|
+
ctx.lineTo(fullscreenX + 5, fullscreenY + 15);
|
|
355
|
+
ctx.lineTo(fullscreenX + 15, fullscreenY + 15);
|
|
356
|
+
ctx.lineTo(fullscreenX + 15, fullscreenY + 5);
|
|
357
|
+
ctx.stroke();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
ctx.restore();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Formate un temps en minutes:secondes
|
|
365
|
+
* @param {number} seconds - Secondes
|
|
366
|
+
* @returns {string} Temps formaté
|
|
367
|
+
* @private
|
|
368
|
+
*/
|
|
369
|
+
formatTime(seconds) {
|
|
370
|
+
const mins = Math.floor(seconds / 60);
|
|
371
|
+
const secs = Math.floor(seconds % 60);
|
|
372
|
+
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Vérifie si un point est dans les limites
|
|
377
|
+
* @param {number} x - Coordonnée X
|
|
378
|
+
* @param {number} y - Coordonnée Y
|
|
379
|
+
* @returns {boolean} True si le point est dans le lecteur
|
|
380
|
+
*/
|
|
381
|
+
isPointInside(x, y) {
|
|
382
|
+
// Le VideoPlayer est cliquable pour les contrôles
|
|
383
|
+
return x >= this.x && x <= this.x + this.width &&
|
|
384
|
+
y >= this.y && y <= this.y + this.height;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Nettoie l'élément vidéo du DOM
|
|
389
|
+
*/
|
|
390
|
+
remove() {
|
|
391
|
+
if (this.videoElement && this.videoElement.parentNode) {
|
|
392
|
+
this.videoElement.parentNode.removeChild(this.videoElement);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export default Video;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Container avec système de layout
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @property {Component[]} children - Enfants
|
|
7
|
+
* @property {number} padding - Padding interne
|
|
8
|
+
* @property {number} gap - Espacement entre enfants
|
|
9
|
+
* @property {string} direction - Direction ('column' ou 'row')
|
|
10
|
+
* @property {string} align - Alignement ('start', 'center', 'end')
|
|
11
|
+
* @property {string} bgColor - Couleur de fond
|
|
12
|
+
* @property {number} borderRadius - Rayon des coins
|
|
13
|
+
*/
|
|
14
|
+
class View extends Component {
|
|
15
|
+
/**
|
|
16
|
+
* Crée une instance de View
|
|
17
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
18
|
+
* @param {Object} [options={}] - Options de configuration
|
|
19
|
+
* @param {number} [options.padding=0] - Padding interne
|
|
20
|
+
* @param {number} [options.gap=0] - Espacement entre enfants
|
|
21
|
+
* @param {string} [options.direction='column'] - Direction
|
|
22
|
+
* @param {string} [options.align='start'] - Alignement
|
|
23
|
+
* @param {string} [options.bgColor='transparent'] - Couleur de fond
|
|
24
|
+
* @param {number} [options.borderRadius=0] - Rayon des coins
|
|
25
|
+
*/
|
|
26
|
+
constructor(framework, options = {}) {
|
|
27
|
+
super(framework, options);
|
|
28
|
+
this.children = [];
|
|
29
|
+
this.padding = options.padding || 0;
|
|
30
|
+
this.gap = options.gap || 0;
|
|
31
|
+
this.direction = options.direction || 'column'; // 'column' ou 'row'
|
|
32
|
+
this.align = options.align || 'start'; // 'start', 'center', 'end'
|
|
33
|
+
this.bgColor = options.bgColor || 'transparent';
|
|
34
|
+
this.borderRadius = options.borderRadius || 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Ajoute un enfant
|
|
39
|
+
* @param {Component} child - Composant enfant
|
|
40
|
+
* @returns {Component} L'enfant ajouté
|
|
41
|
+
*/
|
|
42
|
+
add(child) {
|
|
43
|
+
this.children.push(child);
|
|
44
|
+
this.layout();
|
|
45
|
+
return child;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Organise les enfants selon le layout
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
layout() {
|
|
53
|
+
let currentX = this.x + this.padding;
|
|
54
|
+
let currentY = this.y + this.padding;
|
|
55
|
+
|
|
56
|
+
for (let child of this.children) {
|
|
57
|
+
if (this.direction === 'column') {
|
|
58
|
+
child.x = currentX;
|
|
59
|
+
child.y = currentY;
|
|
60
|
+
if (this.align === 'center') {
|
|
61
|
+
child.x = this.x + (this.width - child.width) / 2;
|
|
62
|
+
} else if (this.align === 'end') {
|
|
63
|
+
child.x = this.x + this.width - child.width - this.padding;
|
|
64
|
+
}
|
|
65
|
+
currentY += child.height + this.gap;
|
|
66
|
+
} else {
|
|
67
|
+
child.x = currentX;
|
|
68
|
+
child.y = currentY;
|
|
69
|
+
if (this.align === 'center') {
|
|
70
|
+
child.y = this.y + (this.height - child.height) / 2;
|
|
71
|
+
} else if (this.align === 'end') {
|
|
72
|
+
child.y = this.y + this.height - child.height - this.padding;
|
|
73
|
+
}
|
|
74
|
+
currentX += child.width + this.gap;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Dessine la vue et ses enfants
|
|
81
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
82
|
+
*/
|
|
83
|
+
draw(ctx) {
|
|
84
|
+
ctx.save();
|
|
85
|
+
|
|
86
|
+
if (this.bgColor !== 'transparent') {
|
|
87
|
+
ctx.fillStyle = this.bgColor;
|
|
88
|
+
if (this.borderRadius > 0) {
|
|
89
|
+
ctx.beginPath();
|
|
90
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
|
|
91
|
+
ctx.fill();
|
|
92
|
+
} else {
|
|
93
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (let child of this.children) {
|
|
98
|
+
if (child.visible) child.draw(ctx);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ctx.restore();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Dessine un rectangle avec coins arrondis
|
|
106
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
107
|
+
* @param {number} x - Position X
|
|
108
|
+
* @param {number} y - Position Y
|
|
109
|
+
* @param {number} width - Largeur
|
|
110
|
+
* @param {number} height - Hauteur
|
|
111
|
+
* @param {number} radius - Rayon des coins
|
|
112
|
+
* @private
|
|
113
|
+
*/
|
|
114
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
115
|
+
ctx.moveTo(x + radius, y);
|
|
116
|
+
ctx.lineTo(x + width - radius, y);
|
|
117
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
118
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
119
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
120
|
+
ctx.lineTo(x + radius, y + height);
|
|
121
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
122
|
+
ctx.lineTo(x, y + radius);
|
|
123
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Vérifie si un point est dans les limites
|
|
128
|
+
* @param {number} x - Coordonnée X
|
|
129
|
+
* @param {number} y - Coordonnée Y
|
|
130
|
+
* @returns {boolean} True si le point est dans la vue
|
|
131
|
+
*/
|
|
132
|
+
isPointInside(x, y) {
|
|
133
|
+
return x >= this.x &&
|
|
134
|
+
x <= this.x + this.width &&
|
|
135
|
+
y >= this.y &&
|
|
136
|
+
y <= this.y + this.height;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default View;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
import ListItem from '../components/ListItem.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Virtual List : optimise le rendu pour les longues listes
|
|
6
|
+
* @class
|
|
7
|
+
* @extends Component
|
|
8
|
+
*/
|
|
9
|
+
class VirtualList extends Component {
|
|
10
|
+
constructor(framework, options = {}) {
|
|
11
|
+
super(framework, options);
|
|
12
|
+
|
|
13
|
+
this.allItemsData = []; // Stocke toutes les données des items (mais pas tous les objets)
|
|
14
|
+
this.visibleItems = []; // Liste des ListItem réellement créés/dessinés
|
|
15
|
+
this.itemHeight = options.itemHeight || 56;
|
|
16
|
+
this.onItemClick = options.onItemClick;
|
|
17
|
+
this.y = options.y || 0;
|
|
18
|
+
|
|
19
|
+
this.viewportHeight = options.height || framework.height; // Hauteur visible
|
|
20
|
+
this.scrollOffset = 0; // Position de scroll
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ajoute un item (seule la data est stockée)
|
|
25
|
+
* @param {Object} itemOptions
|
|
26
|
+
*/
|
|
27
|
+
addItem(itemOptions) {
|
|
28
|
+
this.allItemsData.push(itemOptions);
|
|
29
|
+
this.updateVisibleItems();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Supprime tous les items
|
|
34
|
+
*/
|
|
35
|
+
clear() {
|
|
36
|
+
for (let item of this.visibleItems) {
|
|
37
|
+
this.framework.remove(item);
|
|
38
|
+
}
|
|
39
|
+
this.allItemsData = [];
|
|
40
|
+
this.visibleItems = [];
|
|
41
|
+
this.height = 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Met à jour la liste des items visibles selon scrollOffset
|
|
46
|
+
*/
|
|
47
|
+
updateVisibleItems() {
|
|
48
|
+
const firstIndex = Math.floor(this.scrollOffset / this.itemHeight);
|
|
49
|
+
const lastIndex = Math.min(this.allItemsData.length - 1, Math.ceil((this.scrollOffset + this.viewportHeight) / this.itemHeight));
|
|
50
|
+
|
|
51
|
+
const newVisibleItems = [];
|
|
52
|
+
|
|
53
|
+
for (let i = firstIndex; i <= lastIndex; i++) {
|
|
54
|
+
let item = this.visibleItems.find(v => v.__virtualIndex === i);
|
|
55
|
+
if (!item) {
|
|
56
|
+
// Crée un nouvel item si pas existant
|
|
57
|
+
const data = this.allItemsData[i];
|
|
58
|
+
item = new ListItem(this.framework, {
|
|
59
|
+
...data,
|
|
60
|
+
x: this.x,
|
|
61
|
+
y: this.y + i * this.itemHeight - this.scrollOffset,
|
|
62
|
+
width: this.width,
|
|
63
|
+
height: this.itemHeight,
|
|
64
|
+
onClick: () => {
|
|
65
|
+
if (this.onItemClick) this.onItemClick(i, data);
|
|
66
|
+
if (data.onClick) data.onClick();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
item.__virtualIndex = i;
|
|
70
|
+
this.framework.add(item);
|
|
71
|
+
} else {
|
|
72
|
+
// Met à jour la position Y si déjà existant
|
|
73
|
+
item.y = this.y + i * this.itemHeight - this.scrollOffset;
|
|
74
|
+
}
|
|
75
|
+
newVisibleItems.push(item);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Supprime les items qui ne sont plus visibles
|
|
79
|
+
for (let item of this.visibleItems) {
|
|
80
|
+
if (!newVisibleItems.includes(item)) {
|
|
81
|
+
this.framework.remove(item);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.visibleItems = newVisibleItems;
|
|
86
|
+
this.height = this.allItemsData.length * this.itemHeight;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Scroll la liste
|
|
91
|
+
* @param {number} deltaY
|
|
92
|
+
*/
|
|
93
|
+
scroll(deltaY) {
|
|
94
|
+
this.scrollOffset += deltaY;
|
|
95
|
+
if (this.scrollOffset < 0) this.scrollOffset = 0;
|
|
96
|
+
const maxScroll = Math.max(0, this.height - this.viewportHeight);
|
|
97
|
+
if (this.scrollOffset > maxScroll) this.scrollOffset = maxScroll;
|
|
98
|
+
|
|
99
|
+
this.updateVisibleItems();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Dessine les items visibles
|
|
104
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
105
|
+
*/
|
|
106
|
+
draw(ctx) {
|
|
107
|
+
for (let item of this.visibleItems) {
|
|
108
|
+
item.draw(ctx);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Toujours false : les ListItems gèrent leurs clics
|
|
114
|
+
*/
|
|
115
|
+
isPointInside() {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default VirtualList;
|