canvasframework 0.3.14 → 0.3.15

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.
@@ -1,6 +1,7 @@
1
1
  import Component from '../core/Component.js';
2
+
2
3
  /**
3
- * Spinner de chargement circulaire
4
+ * Spinner de chargement circulaire avec support Material et Cupertino
4
5
  * @class
5
6
  * @extends Component
6
7
  * @property {number} size - Taille du spinner
@@ -21,8 +22,8 @@ class CircularProgress extends Component {
21
22
  * @param {boolean} [options.indeterminate=true] - Mode indéterminé
22
23
  * @param {number} [options.progress=0] - Progression (0-100)
23
24
  * @param {string} [options.color] - Couleur (auto selon platform)
24
- * @param {number} [options.lineWidth=4] - Épaisseur
25
- * @param {number} [options.animationSpeed=0.05] - Vitesse d'animation
25
+ * @param {number} [options.lineWidth] - Épaisseur (auto selon platform)
26
+ * @param {number} [options.animationSpeed] - Vitesse d'animation (auto selon platform)
26
27
  */
27
28
  constructor(framework, options = {}) {
28
29
  super(framework, options);
@@ -30,10 +31,32 @@ class CircularProgress extends Component {
30
31
  this.indeterminate = options.indeterminate !== false;
31
32
  this.progress = options.progress || 0; // 0-100
32
33
  this.platform = framework.platform;
33
- this.color = options.color || (framework.platform === 'material' ? '#6200EE' : '#007AFF');
34
- this.lineWidth = options.lineWidth || 4;
34
+
35
+ // Couleurs selon la plateforme
36
+ this.color = options.color || (
37
+ this.platform === 'material' ? '#6200EE' : '#8E8E93'
38
+ );
39
+
40
+ // Épaisseur selon la plateforme
41
+ this.lineWidth = options.lineWidth || (
42
+ this.platform === 'material' ? 4 : 2.5
43
+ );
44
+
45
+ // Vitesse d'animation selon la plateforme
46
+ this.animationSpeed = options.animationSpeed || (
47
+ this.platform === 'material' ? 0.05 : 0.08
48
+ );
49
+
35
50
  this.rotation = 0;
36
- this.animationSpeed = options.animationSpeed || 0.05;
51
+
52
+ // Pour l'animation Material (arc qui s'agrandit/rétrécit)
53
+ this.arcStart = 0;
54
+ this.arcEnd = 0;
55
+ this.arcGrowing = true;
56
+
57
+ // Pour l'animation Cupertino (12 traits qui tournent)
58
+ this.cupertinoLines = 12;
59
+ this.cupertinoOpacity = Array(this.cupertinoLines).fill(0);
37
60
 
38
61
  this.width = this.size;
39
62
  this.height = this.size;
@@ -49,54 +72,164 @@ class CircularProgress extends Component {
49
72
  * @private
50
73
  */
51
74
  startAnimation() {
52
- const animate = () => {
75
+ let lastTime = performance.now();
76
+
77
+ const animate = (currentTime) => {
53
78
  if (!this.visible || !this.indeterminate) return;
54
79
 
55
- this.rotation += this.animationSpeed;
56
- if (this.rotation > Math.PI * 2) {
57
- this.rotation = 0;
80
+ const deltaTime = currentTime - lastTime;
81
+ lastTime = currentTime;
82
+
83
+ if (this.platform === 'material') {
84
+ this.animateMaterial(deltaTime);
85
+ } else {
86
+ this.animateCupertino(deltaTime);
58
87
  }
59
88
 
89
+ this.markDirty();
60
90
  requestAnimationFrame(animate);
61
91
  };
62
- animate();
92
+
93
+ requestAnimationFrame(animate);
63
94
  }
64
95
 
65
96
  /**
66
- * Dessine le spinner
67
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
97
+ * Animation Material (arc qui tourne et change de taille)
98
+ * @private
68
99
  */
69
- draw(ctx) {
70
- ctx.save();
100
+ animateMaterial(deltaTime) {
101
+ // Rotation globale
102
+ this.rotation += this.animationSpeed;
103
+ if (this.rotation > Math.PI * 2) {
104
+ this.rotation -= Math.PI * 2;
105
+ }
71
106
 
72
- const centerX = this.x + this.size / 2;
73
- const centerY = this.y + this.size / 2;
74
- const radius = (this.size - this.lineWidth) / 2;
107
+ // Animation de l'arc (grossit puis rétrécit)
108
+ const arcSpeed = 0.03;
75
109
 
110
+ if (this.arcGrowing) {
111
+ this.arcEnd += arcSpeed;
112
+ if (this.arcEnd > Math.PI * 1.5) {
113
+ this.arcGrowing = false;
114
+ }
115
+ } else {
116
+ this.arcStart += arcSpeed;
117
+ if (this.arcStart >= this.arcEnd) {
118
+ this.arcGrowing = true;
119
+ this.arcStart = 0;
120
+ this.arcEnd = 0;
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Animation Cupertino (traits qui s'estompent)
127
+ * @private
128
+ */
129
+ animateCupertino(deltaTime) {
130
+ this.rotation += this.animationSpeed;
131
+ if (this.rotation > Math.PI * 2) {
132
+ this.rotation -= Math.PI * 2;
133
+ }
134
+
135
+ // Calculer l'opacité de chaque trait (fade progressif)
136
+ const activeIndex = Math.floor((this.rotation / (Math.PI * 2)) * this.cupertinoLines);
137
+
138
+ for (let i = 0; i < this.cupertinoLines; i++) {
139
+ const distance = Math.abs(i - activeIndex);
140
+ const minDistance = Math.min(distance, this.cupertinoLines - distance);
141
+ this.cupertinoOpacity[i] = 1 - (minDistance / this.cupertinoLines) * 0.8;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Dessine le spinner Material
147
+ * @private
148
+ */
149
+ drawMaterial(ctx, centerX, centerY, radius) {
76
150
  if (this.indeterminate) {
77
- // Spinner qui tourne
151
+ // Track (cercle de base - très léger)
152
+ ctx.strokeStyle = 'rgba(98, 0, 238, 0.1)';
153
+ ctx.lineWidth = this.lineWidth;
154
+ ctx.beginPath();
155
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
156
+ ctx.stroke();
157
+
158
+ // Arc animé qui tourne
159
+ ctx.save();
78
160
  ctx.translate(centerX, centerY);
79
161
  ctx.rotate(this.rotation);
80
- ctx.translate(-centerX, -centerY);
81
162
 
82
- // Cercle de base (track)
83
- ctx.strokeStyle = this.platform === 'material' ? '#E0E0E0' : '#E5E5EA';
84
- ctx.lineWidth = this.lineWidth;
163
+ ctx.strokeStyle = this.color;
85
164
  ctx.lineCap = 'round';
165
+ ctx.lineWidth = this.lineWidth;
166
+ ctx.beginPath();
167
+ ctx.arc(0, 0, radius, this.arcStart, this.arcEnd);
168
+ ctx.stroke();
169
+
170
+ ctx.restore();
171
+ } else {
172
+ // Progress circulaire déterminé
173
+ // Track
174
+ ctx.strokeStyle = '#E0E0E0';
175
+ ctx.lineWidth = this.lineWidth;
86
176
  ctx.beginPath();
87
177
  ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
88
178
  ctx.stroke();
89
179
 
90
- // Arc animé
180
+ // Progress
181
+ const angle = (this.progress / 100) * Math.PI * 2;
91
182
  ctx.strokeStyle = this.color;
183
+ ctx.lineCap = 'round';
92
184
  ctx.beginPath();
93
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 1.5);
185
+ ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + angle);
94
186
  ctx.stroke();
95
187
 
188
+ // Pourcentage au centre
189
+ if (this.progress > 0) {
190
+ ctx.fillStyle = this.color;
191
+ ctx.font = `bold ${this.size / 3}px -apple-system, sans-serif`;
192
+ ctx.textAlign = 'center';
193
+ ctx.textBaseline = 'middle';
194
+ ctx.fillText(`${Math.round(this.progress)}%`, centerX, centerY);
195
+ }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Dessine le spinner Cupertino (style iOS)
201
+ * @private
202
+ */
203
+ drawCupertino(ctx, centerX, centerY, radius) {
204
+ if (this.indeterminate) {
205
+ // Spinner iOS avec 12 traits
206
+ ctx.lineCap = 'round';
207
+ ctx.lineWidth = this.lineWidth;
208
+
209
+ for (let i = 0; i < this.cupertinoLines; i++) {
210
+ const angle = (i / this.cupertinoLines) * Math.PI * 2;
211
+ const opacity = this.cupertinoOpacity[i];
212
+
213
+ ctx.save();
214
+ ctx.translate(centerX, centerY);
215
+ ctx.rotate(angle);
216
+
217
+ // Trait avec opacité variable
218
+ const startRadius = radius * 0.6;
219
+ const endRadius = radius;
220
+
221
+ ctx.strokeStyle = this.hexToRgba(this.color, opacity);
222
+ ctx.beginPath();
223
+ ctx.moveTo(0, -startRadius);
224
+ ctx.lineTo(0, -endRadius);
225
+ ctx.stroke();
226
+
227
+ ctx.restore();
228
+ }
96
229
  } else {
97
- // Progress circulaire déterminé
230
+ // Progress circulaire iOS (plus fin et élégant)
98
231
  // Track
99
- ctx.strokeStyle = this.platform === 'material' ? '#E0E0E0' : '#E5E5EA';
232
+ ctx.strokeStyle = '#E5E5EA';
100
233
  ctx.lineWidth = this.lineWidth;
101
234
  ctx.beginPath();
102
235
  ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
@@ -110,25 +243,76 @@ class CircularProgress extends Component {
110
243
  ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + angle);
111
244
  ctx.stroke();
112
245
 
113
- // Pourcentage au centre (optionnel)
246
+ // Pas de texte au centre pour iOS (plus minimaliste)
247
+ // Mais si tu veux, décommente :
248
+ /*
114
249
  if (this.progress > 0) {
115
250
  ctx.fillStyle = this.color;
116
- ctx.font = `bold ${this.size / 3}px -apple-system, sans-serif`;
251
+ ctx.font = `${this.size / 4}px -apple-system, sans-serif`;
117
252
  ctx.textAlign = 'center';
118
253
  ctx.textBaseline = 'middle';
119
254
  ctx.fillText(`${Math.round(this.progress)}%`, centerX, centerY);
120
255
  }
256
+ */
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Dessine le spinner
262
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
263
+ */
264
+ draw(ctx) {
265
+ ctx.save();
266
+
267
+ const centerX = this.x + this.size / 2;
268
+ const centerY = this.y + this.size / 2;
269
+ const radius = (this.size - this.lineWidth * 2) / 2;
270
+
271
+ if (this.platform === 'material') {
272
+ this.drawMaterial(ctx, centerX, centerY, radius);
273
+ } else {
274
+ this.drawCupertino(ctx, centerX, centerY, radius);
121
275
  }
122
276
 
123
277
  ctx.restore();
124
278
  }
125
279
 
280
+ /**
281
+ * Convertit une couleur hex en rgba avec opacité
282
+ * @private
283
+ */
284
+ hexToRgba(hex, alpha) {
285
+ // Si c'est déjà rgba, le retourner
286
+ if (hex.startsWith('rgba')) return hex;
287
+ if (hex.startsWith('rgb')) {
288
+ return hex.replace('rgb', 'rgba').replace(')', `, ${alpha})`);
289
+ }
290
+
291
+ // Convertir hex en rgba
292
+ const r = parseInt(hex.slice(1, 3), 16);
293
+ const g = parseInt(hex.slice(3, 5), 16);
294
+ const b = parseInt(hex.slice(5, 7), 16);
295
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
296
+ }
297
+
126
298
  /**
127
299
  * Définit la progression
128
300
  * @param {number} value - Valeur de progression (0-100)
129
301
  */
130
302
  setProgress(value) {
131
303
  this.progress = Math.max(0, Math.min(100, value));
304
+ this.indeterminate = false;
305
+ this.markDirty();
306
+ }
307
+
308
+ /**
309
+ * Active le mode indéterminé
310
+ */
311
+ setIndeterminate() {
312
+ this.indeterminate = true;
313
+ if (!this._animating) {
314
+ this.startAnimation();
315
+ }
132
316
  }
133
317
 
134
318
  /**
@@ -66,6 +66,7 @@ import FetchClient from '../utils/FetchClient.js';
66
66
  import GeoLocationService from '../utils/GeoLocationService.js';
67
67
  import WebSocketClient from '../utils/WebSocketClient.js';
68
68
  import AnimationEngine from '../utils/AnimationEngine.js';
69
+ import CryptoManager from '../utils/CryptoManager.js';
69
70
 
70
71
  // Features
71
72
  import PullToRefresh from '../features/PullToRefresh.js';
package/core/Component.js CHANGED
@@ -46,8 +46,98 @@ class Component {
46
46
 
47
47
  // Pour détecter les updates
48
48
  this._prevProps = { ...options };
49
+
50
+ // Système de listeners
51
+ this._listeners = new Map();
49
52
  }
50
53
 
54
+ /**
55
+ * Ajoute un listener pour un événement
56
+ * @param {string} event - Nom de l'événement
57
+ * @param {Function} handler - Fonction callback
58
+ * @returns {Component} - Pour le chaînage
59
+ */
60
+ on(event, handler) {
61
+ if (!this._listeners.has(event)) {
62
+ this._listeners.set(event, []);
63
+ }
64
+ this._listeners.get(event).push(handler);
65
+ return this;
66
+ }
67
+
68
+ /**
69
+ * Retire un listener
70
+ * @param {string} event - Nom de l'événement
71
+ * @param {Function} handler - Fonction à retirer
72
+ * @returns {Component}
73
+ */
74
+ off(event, handler) {
75
+ if (!this._listeners.has(event)) return this;
76
+
77
+ const handlers = this._listeners.get(event);
78
+ const index = handlers.indexOf(handler);
79
+ if (index > -1) {
80
+ handlers.splice(index, 1);
81
+ }
82
+ return this;
83
+ }
84
+
85
+ /**
86
+ * Ajoute un listener qui s'exécute une seule fois
87
+ * @param {string} event - Nom de l'événement
88
+ * @param {Function} handler - Fonction callback
89
+ * @returns {Component}
90
+ */
91
+ once(event, handler) {
92
+ const wrapper = (...args) => {
93
+ handler(...args);
94
+ this.off(event, wrapper);
95
+ };
96
+ return this.on(event, wrapper);
97
+ }
98
+
99
+ /**
100
+ * Émet un événement
101
+ * @param {string} event - Nom de l'événement
102
+ * @param {...any} args - Arguments à passer aux handlers
103
+ * @returns {Component}
104
+ */
105
+ emit(event, ...args) {
106
+ if (!this._listeners.has(event)) return this;
107
+
108
+ const handlers = this._listeners.get(event);
109
+ for (let handler of handlers) {
110
+ try {
111
+ handler(...args);
112
+ } catch (error) {
113
+ console.error(`Error in ${event} handler:`, error);
114
+ }
115
+ }
116
+ return this;
117
+ }
118
+
119
+ /**
120
+ * Retire tous les listeners d'un événement (ou tous)
121
+ * @param {string} [event] - Nom de l'événement (optionnel)
122
+ * @returns {Component}
123
+ */
124
+ removeAllListeners(event) {
125
+ if (event) {
126
+ this._listeners.delete(event);
127
+ } else {
128
+ this._listeners.clear();
129
+ }
130
+ return this;
131
+ }
132
+
133
+ /**
134
+ * Retourne le nombre de listeners pour un événement
135
+ * @param {string} event - Nom de l'événement
136
+ * @returns {number}
137
+ */
138
+ listenerCount(event) {
139
+ return this._listeners.has(event) ? this._listeners.get(event).length : 0;
140
+ }
51
141
  /* =======================
52
142
  LIFECYCLE HOOKS
53
143
  ======================= */
package/index.js CHANGED
@@ -74,6 +74,7 @@ export { default as FetchClient } from './utils/FetchClient.js';
74
74
  export { default as GeoLocationService } from './utils/GeoLocationService.js';
75
75
  export { default as WebSocketClient } from './utils/WebSocketClient.js';
76
76
  export { default as AnimationEngine } from './utils/AnimationEngine.js';
77
+ export { default as CryptoManager } from './utils/CryptoManager.js';
77
78
 
78
79
  // Features
79
80
  export { default as PullToRefresh } from './features/PullToRefresh.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "Canvas-based cross-platform UI framework (Material & Cupertino)",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Gestionnaire de cryptographie pour le Canvas Framework
3
+ * Utilise l'API Web Crypto (SubtleCrypto) pour un chiffrement sécurisé
4
+ * @class
5
+ */
6
+ class CryptoManager {
7
+ constructor() {
8
+ // Vérifier la disponibilité de l'API Crypto
9
+ if (!window.crypto || !window.crypto.subtle) {
10
+ throw new Error('Web Crypto API is not available in this browser');
11
+ }
12
+
13
+ this.crypto = window.crypto.subtle;
14
+
15
+ // Algorithme par défaut
16
+ this.algorithm = {
17
+ name: 'AES-GCM',
18
+ length: 256
19
+ };
20
+
21
+ // Stockage des clés en mémoire (pas dans localStorage !)
22
+ this._keys = new Map();
23
+ }
24
+
25
+ /**
26
+ * Génère une clé de chiffrement aléatoire
27
+ * @param {string} keyName - Nom de la clé pour référence ultérieure
28
+ * @returns {Promise<CryptoKey>}
29
+ */
30
+ async generateKey(keyName = 'default') {
31
+ const key = await this.crypto.generateKey(
32
+ {
33
+ name: this.algorithm.name,
34
+ length: this.algorithm.length
35
+ },
36
+ true, // extractable
37
+ ['encrypt', 'decrypt']
38
+ );
39
+
40
+ this._keys.set(keyName, key);
41
+ return key;
42
+ }
43
+
44
+ /**
45
+ * Importe une clé depuis une chaîne (base64)
46
+ * @param {string} keyString - Clé au format base64
47
+ * @param {string} keyName - Nom de la clé
48
+ * @returns {Promise<CryptoKey>}
49
+ */
50
+ async importKey(keyString, keyName = 'default') {
51
+ const keyBuffer = this._base64ToBuffer(keyString);
52
+
53
+ const key = await this.crypto.importKey(
54
+ 'raw',
55
+ keyBuffer,
56
+ { name: this.algorithm.name },
57
+ true,
58
+ ['encrypt', 'decrypt']
59
+ );
60
+
61
+ this._keys.set(keyName, key);
62
+ return key;
63
+ }
64
+
65
+ /**
66
+ * Exporte une clé au format base64
67
+ * @param {string} keyName - Nom de la clé
68
+ * @returns {Promise<string>}
69
+ */
70
+ async exportKey(keyName = 'default') {
71
+ const key = this._keys.get(keyName);
72
+ if (!key) {
73
+ throw new Error(`Key "${keyName}" not found`);
74
+ }
75
+
76
+ const exported = await this.crypto.exportKey('raw', key);
77
+ return this._bufferToBase64(exported);
78
+ }
79
+
80
+ /**
81
+ * Dérive une clé depuis un mot de passe
82
+ * @param {string} password - Mot de passe
83
+ * @param {string} keyName - Nom de la clé
84
+ * @param {string} salt - Salt (optionnel, généré automatiquement)
85
+ * @returns {Promise<{key: CryptoKey, salt: string}>}
86
+ */
87
+ async deriveKeyFromPassword(password, keyName = 'default', salt = null) {
88
+ // Générer ou utiliser le salt fourni
89
+ const saltBuffer = salt
90
+ ? this._base64ToBuffer(salt)
91
+ : window.crypto.getRandomValues(new Uint8Array(16));
92
+
93
+ // Encoder le mot de passe
94
+ const encoder = new TextEncoder();
95
+ const passwordBuffer = encoder.encode(password);
96
+
97
+ // Importer le mot de passe comme clé
98
+ const keyMaterial = await this.crypto.importKey(
99
+ 'raw',
100
+ passwordBuffer,
101
+ { name: 'PBKDF2' },
102
+ false,
103
+ ['deriveBits', 'deriveKey']
104
+ );
105
+
106
+ // Dériver la clé
107
+ const key = await this.crypto.deriveKey(
108
+ {
109
+ name: 'PBKDF2',
110
+ salt: saltBuffer,
111
+ iterations: 100000,
112
+ hash: 'SHA-256'
113
+ },
114
+ keyMaterial,
115
+ { name: this.algorithm.name, length: this.algorithm.length },
116
+ true,
117
+ ['encrypt', 'decrypt']
118
+ );
119
+
120
+ this._keys.set(keyName, key);
121
+
122
+ return {
123
+ key,
124
+ salt: this._bufferToBase64(saltBuffer)
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Chiffre des données
130
+ * @param {any} data - Données à chiffrer (sera converti en JSON)
131
+ * @param {string} keyName - Nom de la clé à utiliser
132
+ * @returns {Promise<{encrypted: string, iv: string}>}
133
+ */
134
+ async encrypt(data, keyName = 'default') {
135
+ const key = this._keys.get(keyName);
136
+ if (!key) {
137
+ throw new Error(`Key "${keyName}" not found. Generate or import a key first.`);
138
+ }
139
+
140
+ // Convertir les données en JSON puis en buffer
141
+ const dataString = typeof data === 'string' ? data : JSON.stringify(data);
142
+ const encoder = new TextEncoder();
143
+ const dataBuffer = encoder.encode(dataString);
144
+
145
+ // Générer un IV (Initialization Vector) aléatoire
146
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
147
+
148
+ // Chiffrer
149
+ const encryptedBuffer = await this.crypto.encrypt(
150
+ {
151
+ name: this.algorithm.name,
152
+ iv: iv
153
+ },
154
+ key,
155
+ dataBuffer
156
+ );
157
+
158
+ return {
159
+ encrypted: this._bufferToBase64(encryptedBuffer),
160
+ iv: this._bufferToBase64(iv)
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Déchiffre des données
166
+ * @param {string} encryptedData - Données chiffrées (base64)
167
+ * @param {string} iv - IV utilisé lors du chiffrement (base64)
168
+ * @param {string} keyName - Nom de la clé à utiliser
169
+ * @param {boolean} parseJson - Parser le résultat en JSON (true par défaut)
170
+ * @returns {Promise<any>}
171
+ */
172
+ async decrypt(encryptedData, iv, keyName = 'default', parseJson = true) {
173
+ const key = this._keys.get(keyName);
174
+ if (!key) {
175
+ throw new Error(`Key "${keyName}" not found. Generate or import a key first.`);
176
+ }
177
+
178
+ // Convertir les chaînes base64 en buffers
179
+ const encryptedBuffer = this._base64ToBuffer(encryptedData);
180
+ const ivBuffer = this._base64ToBuffer(iv);
181
+
182
+ // Déchiffrer
183
+ const decryptedBuffer = await this.crypto.decrypt(
184
+ {
185
+ name: this.algorithm.name,
186
+ iv: ivBuffer
187
+ },
188
+ key,
189
+ encryptedBuffer
190
+ );
191
+
192
+ // Convertir le buffer en string
193
+ const decoder = new TextDecoder();
194
+ const decryptedString = decoder.decode(decryptedBuffer);
195
+
196
+ // Parser en JSON si demandé
197
+ if (parseJson) {
198
+ try {
199
+ return JSON.parse(decryptedString);
200
+ } catch (e) {
201
+ // Si le parsing échoue, retourner la chaîne brute
202
+ return decryptedString;
203
+ }
204
+ }
205
+
206
+ return decryptedString;
207
+ }
208
+
209
+ /**
210
+ * Chiffre et encode en une seule chaîne (pratique pour stockage)
211
+ * @param {any} data - Données à chiffrer
212
+ * @param {string} keyName - Nom de la clé
213
+ * @returns {Promise<string>} - Chaîne contenant données chiffrées + IV
214
+ */
215
+ async encryptToString(data, keyName = 'default') {
216
+ const { encrypted, iv } = await this.encrypt(data, keyName);
217
+ return `${encrypted}.${iv}`;
218
+ }
219
+
220
+ /**
221
+ * Déchiffre depuis une chaîne créée par encryptToString
222
+ * @param {string} encryptedString - Chaîne chiffrée
223
+ * @param {string} keyName - Nom de la clé
224
+ * @param {boolean} parseJson - Parser en JSON
225
+ * @returns {Promise<any>}
226
+ */
227
+ async decryptFromString(encryptedString, keyName = 'default', parseJson = true) {
228
+ const [encrypted, iv] = encryptedString.split('.');
229
+ if (!encrypted || !iv) {
230
+ throw new Error('Invalid encrypted string format');
231
+ }
232
+ return this.decrypt(encrypted, iv, keyName, parseJson);
233
+ }
234
+
235
+ /**
236
+ * Hash une chaîne (non réversible)
237
+ * @param {string} data - Données à hasher
238
+ * @param {string} algorithm - Algorithme (SHA-256, SHA-384, SHA-512)
239
+ * @returns {Promise<string>} - Hash en base64
240
+ */
241
+ async hash(data, algorithm = 'SHA-256') {
242
+ const encoder = new TextEncoder();
243
+ const dataBuffer = encoder.encode(data);
244
+
245
+ const hashBuffer = await this.crypto.digest(algorithm, dataBuffer);
246
+ return this._bufferToBase64(hashBuffer);
247
+ }
248
+
249
+ /**
250
+ * Génère un token aléatoire sécurisé
251
+ * @param {number} length - Longueur en octets (32 par défaut)
252
+ * @returns {string} - Token en base64
253
+ */
254
+ generateToken(length = 32) {
255
+ const buffer = window.crypto.getRandomValues(new Uint8Array(length));
256
+ return this._bufferToBase64(buffer);
257
+ }
258
+
259
+ /**
260
+ * Supprime une clé de la mémoire
261
+ * @param {string} keyName - Nom de la clé
262
+ */
263
+ deleteKey(keyName) {
264
+ this._keys.delete(keyName);
265
+ }
266
+
267
+ /**
268
+ * Supprime toutes les clés
269
+ */
270
+ deleteAllKeys() {
271
+ this._keys.clear();
272
+ }
273
+
274
+ /**
275
+ * Liste les clés disponibles
276
+ * @returns {string[]}
277
+ */
278
+ listKeys() {
279
+ return Array.from(this._keys.keys());
280
+ }
281
+
282
+ // ===== Méthodes utilitaires =====
283
+
284
+ _bufferToBase64(buffer) {
285
+ const bytes = new Uint8Array(buffer);
286
+ let binary = '';
287
+ for (let i = 0; i < bytes.length; i++) {
288
+ binary += String.fromCharCode(bytes[i]);
289
+ }
290
+ return btoa(binary);
291
+ }
292
+
293
+ _base64ToBuffer(base64) {
294
+ const binary = atob(base64);
295
+ const bytes = new Uint8Array(binary.length);
296
+ for (let i = 0; i < binary.length; i++) {
297
+ bytes[i] = binary.charCodeAt(i);
298
+ }
299
+ return bytes.buffer;
300
+ }
301
+ }
302
+
303
+ export default CryptoManager;