@umituz/web-canvas 1.0.1 → 1.0.2
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/dist/application/index.d.mts +1 -1
- package/dist/application/index.d.ts +1 -1
- package/dist/application/index.js +103 -40
- package/dist/application/index.mjs +2 -2
- package/dist/{chunk-BN7JTXL7.mjs → chunk-DNKRSL6D.mjs} +24 -12
- package/dist/{chunk-SF6WND7M.mjs → chunk-LOVI2LTO.mjs} +88 -28
- package/dist/{chunk-WMKPCJFI.mjs → chunk-UOGTEMIJ.mjs} +80 -29
- package/dist/index.js +186 -63
- package/dist/index.mjs +3 -3
- package/dist/infrastructure/index.d.mts +30 -6
- package/dist/infrastructure/index.d.ts +30 -6
- package/dist/infrastructure/index.js +80 -29
- package/dist/infrastructure/index.mjs +1 -1
- package/dist/presentation/index.js +186 -63
- package/dist/presentation/index.mjs +3 -3
- package/package.json +1 -1
- package/src/application/services/ScreenshotStudioService.ts +35 -12
- package/src/infrastructure/renderers/CanvasRenderer.ts +30 -3
- package/src/infrastructure/renderers/PatternRenderer.ts +43 -23
- package/src/infrastructure/renderers/ScreenshotRenderer.ts +14 -1
- package/src/infrastructure/services/DeviceConfigService.ts +18 -3
- package/src/presentation/hooks/useCanvasExport.ts +48 -8
- package/src/presentation/hooks/useReportExport.ts +38 -12
- package/src/presentation/hooks/useScreenshotStudio.ts +35 -6
|
@@ -77,6 +77,7 @@ var DEFAULT_CANVAS_CONFIG = {
|
|
|
77
77
|
var CanvasRenderer = class {
|
|
78
78
|
constructor(config = DEFAULT_CANVAS_CONFIG) {
|
|
79
79
|
this.config = { ...DEFAULT_CANVAS_CONFIG, ...config };
|
|
80
|
+
this.imageCache = /* @__PURE__ */ new Map();
|
|
80
81
|
}
|
|
81
82
|
/**
|
|
82
83
|
* Create a new canvas element
|
|
@@ -178,17 +179,37 @@ var CanvasRenderer = class {
|
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
181
|
/**
|
|
181
|
-
* Load image from URL
|
|
182
|
+
* Load image from URL with caching
|
|
182
183
|
*/
|
|
183
184
|
loadImage(url) {
|
|
185
|
+
if (this.imageCache.has(url)) {
|
|
186
|
+
return Promise.resolve(this.imageCache.get(url));
|
|
187
|
+
}
|
|
184
188
|
return new Promise((resolve, reject) => {
|
|
185
189
|
const img = new Image();
|
|
186
|
-
|
|
187
|
-
|
|
190
|
+
if (this.config.useCORS) {
|
|
191
|
+
img.crossOrigin = "anonymous";
|
|
192
|
+
}
|
|
193
|
+
img.onload = () => {
|
|
194
|
+
this.imageCache.set(url, img);
|
|
195
|
+
resolve(img);
|
|
196
|
+
};
|
|
188
197
|
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
|
|
189
198
|
img.src = url;
|
|
190
199
|
});
|
|
191
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Clear image cache to free memory
|
|
203
|
+
*/
|
|
204
|
+
clearImageCache() {
|
|
205
|
+
this.imageCache.clear();
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get cache size for monitoring
|
|
209
|
+
*/
|
|
210
|
+
getCacheSize() {
|
|
211
|
+
return this.imageCache.size;
|
|
212
|
+
}
|
|
192
213
|
/**
|
|
193
214
|
* Create gradient
|
|
194
215
|
*/
|
|
@@ -270,10 +291,15 @@ var CanvasRenderer = class {
|
|
|
270
291
|
|
|
271
292
|
// src/infrastructure/renderers/PatternRenderer.ts
|
|
272
293
|
var PatternRenderer = class {
|
|
294
|
+
constructor() {
|
|
295
|
+
this.patternCanvasCache = /* @__PURE__ */ new Map();
|
|
296
|
+
this.MAX_CACHE_SIZE = 10;
|
|
297
|
+
}
|
|
273
298
|
/**
|
|
274
299
|
* Render pattern on canvas
|
|
275
300
|
*/
|
|
276
301
|
renderPattern(ctx, pattern, dimensions, color = "#000000") {
|
|
302
|
+
if (pattern === "none") return;
|
|
277
303
|
ctx.save();
|
|
278
304
|
ctx.globalAlpha = 0.1;
|
|
279
305
|
switch (pattern) {
|
|
@@ -292,62 +318,65 @@ var PatternRenderer = class {
|
|
|
292
318
|
case "checkers":
|
|
293
319
|
this.renderCheckerPattern(ctx, dimensions, color);
|
|
294
320
|
break;
|
|
295
|
-
case "none":
|
|
296
|
-
default:
|
|
297
|
-
break;
|
|
298
321
|
}
|
|
299
322
|
ctx.restore();
|
|
300
323
|
}
|
|
301
324
|
/**
|
|
302
|
-
*
|
|
325
|
+
* Clear pattern cache to free memory
|
|
326
|
+
*/
|
|
327
|
+
clearCache() {
|
|
328
|
+
this.patternCanvasCache.clear();
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Render dot pattern - optimized with batch rendering
|
|
303
332
|
*/
|
|
304
333
|
renderDotPattern(ctx, dimensions, color) {
|
|
305
334
|
ctx.fillStyle = color;
|
|
306
335
|
const spacing = 30;
|
|
336
|
+
const radius = 2;
|
|
337
|
+
ctx.beginPath();
|
|
307
338
|
for (let x = spacing; x < dimensions.width; x += spacing) {
|
|
308
339
|
for (let y = spacing; y < dimensions.height; y += spacing) {
|
|
309
|
-
ctx.
|
|
310
|
-
ctx.arc(x, y,
|
|
311
|
-
ctx.fill();
|
|
340
|
+
ctx.moveTo(x + radius, y);
|
|
341
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
312
342
|
}
|
|
313
343
|
}
|
|
344
|
+
ctx.fill();
|
|
314
345
|
}
|
|
315
346
|
/**
|
|
316
|
-
* Render grid pattern
|
|
347
|
+
* Render grid pattern - optimized with single path
|
|
317
348
|
*/
|
|
318
349
|
renderGridPattern(ctx, dimensions, color) {
|
|
319
350
|
ctx.strokeStyle = color;
|
|
320
351
|
ctx.lineWidth = 1;
|
|
321
352
|
const spacing = 30;
|
|
353
|
+
ctx.beginPath();
|
|
322
354
|
for (let x = 0; x <= dimensions.width; x += spacing) {
|
|
323
|
-
ctx.beginPath();
|
|
324
355
|
ctx.moveTo(x, 0);
|
|
325
356
|
ctx.lineTo(x, dimensions.height);
|
|
326
|
-
ctx.stroke();
|
|
327
357
|
}
|
|
328
358
|
for (let y = 0; y <= dimensions.height; y += spacing) {
|
|
329
|
-
ctx.beginPath();
|
|
330
359
|
ctx.moveTo(0, y);
|
|
331
360
|
ctx.lineTo(dimensions.width, y);
|
|
332
|
-
ctx.stroke();
|
|
333
361
|
}
|
|
362
|
+
ctx.stroke();
|
|
334
363
|
}
|
|
335
364
|
/**
|
|
336
|
-
* Render line pattern
|
|
365
|
+
* Render line pattern - optimized with single path
|
|
337
366
|
*/
|
|
338
367
|
renderLinePattern(ctx, dimensions, color) {
|
|
339
368
|
ctx.strokeStyle = color;
|
|
340
369
|
ctx.lineWidth = 2;
|
|
341
370
|
const spacing = 20;
|
|
371
|
+
ctx.beginPath();
|
|
342
372
|
for (let y = spacing; y < dimensions.height; y += spacing) {
|
|
343
|
-
ctx.beginPath();
|
|
344
373
|
ctx.moveTo(0, y);
|
|
345
374
|
ctx.lineTo(dimensions.width, y);
|
|
346
|
-
ctx.stroke();
|
|
347
375
|
}
|
|
376
|
+
ctx.stroke();
|
|
348
377
|
}
|
|
349
378
|
/**
|
|
350
|
-
* Render wave pattern
|
|
379
|
+
* Render wave pattern - optimized with pre-calculated values
|
|
351
380
|
*/
|
|
352
381
|
renderWavePattern(ctx, dimensions, color) {
|
|
353
382
|
ctx.strokeStyle = color;
|
|
@@ -355,15 +384,15 @@ var PatternRenderer = class {
|
|
|
355
384
|
const amplitude = 10;
|
|
356
385
|
const frequency = 0.1;
|
|
357
386
|
const spacing = 40;
|
|
387
|
+
const step = 5;
|
|
358
388
|
for (let y = spacing; y < dimensions.height; y += spacing) {
|
|
359
389
|
ctx.beginPath();
|
|
360
|
-
|
|
390
|
+
const startX = 0;
|
|
391
|
+
const startY = y + Math.sin(startX * frequency) * amplitude;
|
|
392
|
+
ctx.moveTo(startX, startY);
|
|
393
|
+
for (let x = step; x <= dimensions.width; x += step) {
|
|
361
394
|
const waveY = y + Math.sin(x * frequency) * amplitude;
|
|
362
|
-
|
|
363
|
-
ctx.moveTo(x, waveY);
|
|
364
|
-
} else {
|
|
365
|
-
ctx.lineTo(x, waveY);
|
|
366
|
-
}
|
|
395
|
+
ctx.lineTo(x, waveY);
|
|
367
396
|
}
|
|
368
397
|
ctx.stroke();
|
|
369
398
|
}
|
|
@@ -405,6 +434,9 @@ var DEFAULT_DEVICE_CONFIG = {
|
|
|
405
434
|
// src/infrastructure/services/DeviceConfigService.ts
|
|
406
435
|
var DeviceConfigService = class _DeviceConfigService {
|
|
407
436
|
constructor() {
|
|
437
|
+
this.phonesCache = null;
|
|
438
|
+
this.tabletsCache = null;
|
|
439
|
+
this.allDevicesCache = null;
|
|
408
440
|
this.devices = /* @__PURE__ */ new Map();
|
|
409
441
|
this.initializeDevices();
|
|
410
442
|
}
|
|
@@ -501,13 +533,22 @@ var DeviceConfigService = class _DeviceConfigService {
|
|
|
501
533
|
return this.devices.get(deviceType);
|
|
502
534
|
}
|
|
503
535
|
getAllDevices() {
|
|
504
|
-
|
|
536
|
+
if (!this.allDevicesCache) {
|
|
537
|
+
this.allDevicesCache = Array.from(this.devices.values());
|
|
538
|
+
}
|
|
539
|
+
return this.allDevicesCache;
|
|
505
540
|
}
|
|
506
541
|
getPhones() {
|
|
507
|
-
|
|
542
|
+
if (!this.phonesCache) {
|
|
543
|
+
this.phonesCache = this.getAllDevices().filter((d) => d.isPhone);
|
|
544
|
+
}
|
|
545
|
+
return this.phonesCache;
|
|
508
546
|
}
|
|
509
547
|
getTablets() {
|
|
510
|
-
|
|
548
|
+
if (!this.tabletsCache) {
|
|
549
|
+
this.tabletsCache = this.getAllDevices().filter((d) => d.isTablet);
|
|
550
|
+
}
|
|
551
|
+
return this.tabletsCache;
|
|
511
552
|
}
|
|
512
553
|
isPhone(deviceType) {
|
|
513
554
|
const config = this.getDeviceConfig(deviceType);
|
|
@@ -526,10 +567,20 @@ var DeviceConfigService = class _DeviceConfigService {
|
|
|
526
567
|
// src/infrastructure/renderers/ScreenshotRenderer.ts
|
|
527
568
|
var ScreenshotRenderer = class {
|
|
528
569
|
constructor() {
|
|
570
|
+
this.renderCache = /* @__PURE__ */ new Map();
|
|
571
|
+
this.MAX_CACHE_SIZE = 5;
|
|
529
572
|
this.canvasRenderer = new CanvasRenderer();
|
|
530
573
|
this.patternRenderer = new PatternRenderer();
|
|
531
574
|
this.deviceConfigService = DeviceConfigService.getInstance();
|
|
532
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Clear render cache to free memory
|
|
578
|
+
*/
|
|
579
|
+
clearCache() {
|
|
580
|
+
this.renderCache.clear();
|
|
581
|
+
this.canvasRenderer.clearImageCache?.();
|
|
582
|
+
this.patternRenderer.clearCache?.();
|
|
583
|
+
}
|
|
533
584
|
/**
|
|
534
585
|
* Render device frame
|
|
535
586
|
*/
|
|
@@ -581,7 +632,7 @@ var ScreenshotRenderer = class {
|
|
|
581
632
|
ctx.fill();
|
|
582
633
|
}
|
|
583
634
|
/**
|
|
584
|
-
* Render background
|
|
635
|
+
* Render background - optimized
|
|
585
636
|
*/
|
|
586
637
|
renderBackground(ctx, design, dimensions) {
|
|
587
638
|
if (design.backgroundGradientEnd) {
|
|
@@ -791,10 +842,17 @@ var ScreenshotStudioService = class {
|
|
|
791
842
|
* Generate batch screenshots
|
|
792
843
|
*/
|
|
793
844
|
async generateBatchScreenshots(deviceTypes) {
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
)
|
|
797
|
-
|
|
845
|
+
const BATCH_SIZE = 3;
|
|
846
|
+
const results = [];
|
|
847
|
+
for (let i = 0; i < deviceTypes.length; i += BATCH_SIZE) {
|
|
848
|
+
const batch = deviceTypes.slice(i, i + BATCH_SIZE);
|
|
849
|
+
const batchPromises = batch.map(
|
|
850
|
+
(deviceType) => this.generateScreenshot({ deviceType })
|
|
851
|
+
);
|
|
852
|
+
const batchResults = await Promise.all(batchPromises);
|
|
853
|
+
results.push(...batchResults);
|
|
854
|
+
}
|
|
855
|
+
return results;
|
|
798
856
|
}
|
|
799
857
|
/**
|
|
800
858
|
* Generate designed screenshot
|
|
@@ -835,16 +893,18 @@ WHATS NEW:
|
|
|
835
893
|
${project.appMetadata.whatsNew}`;
|
|
836
894
|
folder.file("aso_metadata.txt", metadataText);
|
|
837
895
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
896
|
+
const screenshotPromises = project.screens.map(
|
|
897
|
+
(screen, index) => this.generateDesignedScreenshot(screen, project.design).then((screenshot) => ({ screenshot, index }))
|
|
898
|
+
);
|
|
899
|
+
const screenshots = await Promise.all(screenshotPromises);
|
|
900
|
+
screenshots.forEach(({ screenshot, index }) => {
|
|
841
901
|
const base64Data = screenshot.url.split(",")[1];
|
|
842
902
|
folder.file(
|
|
843
|
-
`screen_${
|
|
903
|
+
`screen_${index + 1}_${project.design.deviceType}.png`,
|
|
844
904
|
base64Data,
|
|
845
905
|
{ base64: true }
|
|
846
906
|
);
|
|
847
|
-
}
|
|
907
|
+
});
|
|
848
908
|
return await zip.generateAsync({ type: "blob" });
|
|
849
909
|
}
|
|
850
910
|
/**
|
|
@@ -928,10 +988,13 @@ ${project.appMetadata.whatsNew}`;
|
|
|
928
988
|
};
|
|
929
989
|
}
|
|
930
990
|
/**
|
|
931
|
-
* Generate unique ID
|
|
991
|
+
* Generate unique ID - optimized version
|
|
932
992
|
*/
|
|
933
993
|
generateId() {
|
|
934
|
-
|
|
994
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
995
|
+
return crypto.randomUUID();
|
|
996
|
+
}
|
|
997
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 9)}`;
|
|
935
998
|
}
|
|
936
999
|
};
|
|
937
1000
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ReportService,
|
|
3
3
|
ScreenshotStudioService
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-DNKRSL6D.mjs";
|
|
5
|
+
import "../chunk-UOGTEMIJ.mjs";
|
|
6
6
|
import "../chunk-Z6IB4BQV.mjs";
|
|
7
7
|
import "../chunk-MC6PNV2F.mjs";
|
|
8
8
|
export {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ScreenshotRenderer
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-UOGTEMIJ.mjs";
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_SCREENSHOT_STUDIO_CONFIG
|
|
6
6
|
} from "./chunk-Z6IB4BQV.mjs";
|
|
@@ -54,10 +54,17 @@ var ScreenshotStudioService = class {
|
|
|
54
54
|
* Generate batch screenshots
|
|
55
55
|
*/
|
|
56
56
|
async generateBatchScreenshots(deviceTypes) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
)
|
|
60
|
-
|
|
57
|
+
const BATCH_SIZE = 3;
|
|
58
|
+
const results = [];
|
|
59
|
+
for (let i = 0; i < deviceTypes.length; i += BATCH_SIZE) {
|
|
60
|
+
const batch = deviceTypes.slice(i, i + BATCH_SIZE);
|
|
61
|
+
const batchPromises = batch.map(
|
|
62
|
+
(deviceType) => this.generateScreenshot({ deviceType })
|
|
63
|
+
);
|
|
64
|
+
const batchResults = await Promise.all(batchPromises);
|
|
65
|
+
results.push(...batchResults);
|
|
66
|
+
}
|
|
67
|
+
return results;
|
|
61
68
|
}
|
|
62
69
|
/**
|
|
63
70
|
* Generate designed screenshot
|
|
@@ -98,16 +105,18 @@ WHATS NEW:
|
|
|
98
105
|
${project.appMetadata.whatsNew}`;
|
|
99
106
|
folder.file("aso_metadata.txt", metadataText);
|
|
100
107
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
const screenshotPromises = project.screens.map(
|
|
109
|
+
(screen, index) => this.generateDesignedScreenshot(screen, project.design).then((screenshot) => ({ screenshot, index }))
|
|
110
|
+
);
|
|
111
|
+
const screenshots = await Promise.all(screenshotPromises);
|
|
112
|
+
screenshots.forEach(({ screenshot, index }) => {
|
|
104
113
|
const base64Data = screenshot.url.split(",")[1];
|
|
105
114
|
folder.file(
|
|
106
|
-
`screen_${
|
|
115
|
+
`screen_${index + 1}_${project.design.deviceType}.png`,
|
|
107
116
|
base64Data,
|
|
108
117
|
{ base64: true }
|
|
109
118
|
);
|
|
110
|
-
}
|
|
119
|
+
});
|
|
111
120
|
return await zip.generateAsync({ type: "blob" });
|
|
112
121
|
}
|
|
113
122
|
/**
|
|
@@ -191,10 +200,13 @@ ${project.appMetadata.whatsNew}`;
|
|
|
191
200
|
};
|
|
192
201
|
}
|
|
193
202
|
/**
|
|
194
|
-
* Generate unique ID
|
|
203
|
+
* Generate unique ID - optimized version
|
|
195
204
|
*/
|
|
196
205
|
generateId() {
|
|
197
|
-
|
|
206
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
207
|
+
return crypto.randomUUID();
|
|
208
|
+
}
|
|
209
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 9)}`;
|
|
198
210
|
}
|
|
199
211
|
};
|
|
200
212
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ReportService,
|
|
3
3
|
ScreenshotStudioService
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-DNKRSL6D.mjs";
|
|
5
5
|
import {
|
|
6
6
|
CanvasRenderer
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-UOGTEMIJ.mjs";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_SCREENSHOT_STUDIO_CONFIG
|
|
10
10
|
} from "./chunk-Z6IB4BQV.mjs";
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./chunk-MC6PNV2F.mjs";
|
|
14
14
|
|
|
15
15
|
// src/presentation/hooks/useScreenshotStudio.ts
|
|
16
|
-
import { useState, useCallback, useRef } from "react";
|
|
16
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
17
17
|
var DEFAULT_SCREENS = [
|
|
18
18
|
{
|
|
19
19
|
id: "1",
|
|
@@ -107,9 +107,16 @@ function useScreenshotStudio(options = {}) {
|
|
|
107
107
|
...DEFAULT_SCREENSHOT_STUDIO_CONFIG,
|
|
108
108
|
...config
|
|
109
109
|
};
|
|
110
|
-
const serviceRef = useRef(
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
const serviceRef = useRef(null);
|
|
111
|
+
if (!serviceRef.current) {
|
|
112
|
+
serviceRef.current = new ScreenshotStudioService(fullConfig);
|
|
113
|
+
}
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
return () => {
|
|
116
|
+
if (serviceRef.current) {
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}, []);
|
|
113
120
|
const [screens, setScreens] = useState(DEFAULT_SCREENS);
|
|
114
121
|
const [design, setDesign] = useState(DEFAULT_DESIGN);
|
|
115
122
|
const [currentLanguage, setCurrentLanguage] = useState(
|
|
@@ -230,6 +237,7 @@ function useScreenshotStudio(options = {}) {
|
|
|
230
237
|
);
|
|
231
238
|
const generateDesignedScreenshot = useCallback(
|
|
232
239
|
async (screenId) => {
|
|
240
|
+
if (!serviceRef.current) return;
|
|
233
241
|
const screen = screens.find((s) => s.id === screenId);
|
|
234
242
|
if (!screen) return;
|
|
235
243
|
setIsProcessing(true);
|
|
@@ -250,7 +258,9 @@ function useScreenshotStudio(options = {}) {
|
|
|
250
258
|
);
|
|
251
259
|
const exportAllScreensAsZip = useCallback(
|
|
252
260
|
async (format = "png") => {
|
|
261
|
+
if (!serviceRef.current) return;
|
|
253
262
|
setIsProcessing(true);
|
|
263
|
+
let url = null;
|
|
254
264
|
try {
|
|
255
265
|
const project = {
|
|
256
266
|
id: projectId || "default-project",
|
|
@@ -265,12 +275,11 @@ function useScreenshotStudio(options = {}) {
|
|
|
265
275
|
updatedAt: /* @__PURE__ */ new Date()
|
|
266
276
|
};
|
|
267
277
|
const blob = await serviceRef.current.exportProjectAsZip(project);
|
|
268
|
-
|
|
278
|
+
url = URL.createObjectURL(blob);
|
|
269
279
|
const link = document.createElement("a");
|
|
270
280
|
link.href = url;
|
|
271
281
|
link.download = `Studio_${currentLanguage}_${Date.now()}.zip`;
|
|
272
282
|
link.click();
|
|
273
|
-
URL.revokeObjectURL(url);
|
|
274
283
|
onShowToast(
|
|
275
284
|
"Success",
|
|
276
285
|
`Exported ${screens.length} screens in ZIP archive.`
|
|
@@ -279,6 +288,14 @@ function useScreenshotStudio(options = {}) {
|
|
|
279
288
|
onShowToast("Error", "ZIP export failed");
|
|
280
289
|
} finally {
|
|
281
290
|
setIsProcessing(false);
|
|
291
|
+
if (url) {
|
|
292
|
+
setTimeout(() => {
|
|
293
|
+
try {
|
|
294
|
+
URL.revokeObjectURL(url);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
}
|
|
297
|
+
}, 100);
|
|
298
|
+
}
|
|
282
299
|
}
|
|
283
300
|
},
|
|
284
301
|
[
|
|
@@ -358,21 +375,40 @@ function useScreenshotStudio(options = {}) {
|
|
|
358
375
|
}
|
|
359
376
|
|
|
360
377
|
// src/presentation/hooks/useCanvasExport.ts
|
|
361
|
-
import { useState as useState2, useCallback as useCallback2 } from "react";
|
|
378
|
+
import { useState as useState2, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect2 } from "react";
|
|
362
379
|
function useCanvasExport(options = {}) {
|
|
363
380
|
const { config = DEFAULT_CANVAS_CONFIG } = options;
|
|
364
381
|
const [isExporting, setIsExporting] = useState2(false);
|
|
365
382
|
const [progress, setProgress] = useState2(0);
|
|
366
383
|
const [error, setError] = useState2(null);
|
|
367
|
-
const
|
|
384
|
+
const rendererRef = useRef2(null);
|
|
385
|
+
const urlCleanupRef = useRef2([]);
|
|
386
|
+
if (!rendererRef.current) {
|
|
387
|
+
rendererRef.current = new CanvasRenderer(config);
|
|
388
|
+
}
|
|
389
|
+
useEffect2(() => {
|
|
390
|
+
return () => {
|
|
391
|
+
urlCleanupRef.current.forEach((url) => {
|
|
392
|
+
try {
|
|
393
|
+
URL.revokeObjectURL(url);
|
|
394
|
+
} catch (e) {
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
urlCleanupRef.current = [];
|
|
398
|
+
};
|
|
399
|
+
}, []);
|
|
368
400
|
const exportToBlob = useCallback2(
|
|
369
401
|
async (canvas, options2) => {
|
|
402
|
+
if (!rendererRef.current) return null;
|
|
370
403
|
setIsExporting(true);
|
|
371
404
|
setProgress(0);
|
|
372
405
|
setError(null);
|
|
373
406
|
try {
|
|
374
|
-
const result = await
|
|
407
|
+
const result = await rendererRef.current.exportToBlob(canvas, options2);
|
|
375
408
|
setProgress(100);
|
|
409
|
+
if (result?.url) {
|
|
410
|
+
urlCleanupRef.current.push(result.url);
|
|
411
|
+
}
|
|
376
412
|
return result;
|
|
377
413
|
} catch (err) {
|
|
378
414
|
const errorMessage = err instanceof Error ? err.message : "Export failed";
|
|
@@ -382,19 +418,20 @@ function useCanvasExport(options = {}) {
|
|
|
382
418
|
setIsExporting(false);
|
|
383
419
|
}
|
|
384
420
|
},
|
|
385
|
-
[
|
|
421
|
+
[]
|
|
386
422
|
);
|
|
387
423
|
const exportToDataURL = useCallback2(
|
|
388
424
|
(canvas, format, quality) => {
|
|
425
|
+
if (!rendererRef.current) return null;
|
|
389
426
|
try {
|
|
390
|
-
return
|
|
427
|
+
return rendererRef.current.exportToDataURL(canvas, format, quality);
|
|
391
428
|
} catch (err) {
|
|
392
429
|
const errorMessage = err instanceof Error ? err.message : "Export failed";
|
|
393
430
|
setError(errorMessage);
|
|
394
431
|
return null;
|
|
395
432
|
}
|
|
396
433
|
},
|
|
397
|
-
[
|
|
434
|
+
[]
|
|
398
435
|
);
|
|
399
436
|
const downloadCanvas = useCallback2(
|
|
400
437
|
async (canvas, filename, options2) => {
|
|
@@ -414,6 +451,10 @@ function useCanvasExport(options = {}) {
|
|
|
414
451
|
setProgress(100);
|
|
415
452
|
setTimeout(() => {
|
|
416
453
|
URL.revokeObjectURL(result.url);
|
|
454
|
+
const index = urlCleanupRef.current.indexOf(result.url);
|
|
455
|
+
if (index > -1) {
|
|
456
|
+
urlCleanupRef.current.splice(index, 1);
|
|
457
|
+
}
|
|
417
458
|
}, 100);
|
|
418
459
|
} catch (err) {
|
|
419
460
|
const errorMessage = err instanceof Error ? err.message : "Download failed";
|
|
@@ -426,15 +467,16 @@ function useCanvasExport(options = {}) {
|
|
|
426
467
|
);
|
|
427
468
|
const cloneCanvas = useCallback2(
|
|
428
469
|
(canvas) => {
|
|
470
|
+
if (!rendererRef.current) return null;
|
|
429
471
|
try {
|
|
430
|
-
return
|
|
472
|
+
return rendererRef.current.cloneCanvas(canvas);
|
|
431
473
|
} catch (err) {
|
|
432
474
|
const errorMessage = err instanceof Error ? err.message : "Clone failed";
|
|
433
475
|
setError(errorMessage);
|
|
434
476
|
return null;
|
|
435
477
|
}
|
|
436
478
|
},
|
|
437
|
-
[
|
|
479
|
+
[]
|
|
438
480
|
);
|
|
439
481
|
return {
|
|
440
482
|
isExporting,
|
|
@@ -448,17 +490,23 @@ function useCanvasExport(options = {}) {
|
|
|
448
490
|
}
|
|
449
491
|
|
|
450
492
|
// src/presentation/hooks/useReportExport.ts
|
|
451
|
-
import { useState as useState3, useCallback as useCallback3 } from "react";
|
|
493
|
+
import { useState as useState3, useCallback as useCallback3, useRef as useRef3 } from "react";
|
|
452
494
|
function useReportExport() {
|
|
453
495
|
const [isExporting, setIsExporting] = useState3(false);
|
|
454
496
|
const [error, setError] = useState3(null);
|
|
455
|
-
const
|
|
497
|
+
const reportServiceRef = useRef3(null);
|
|
498
|
+
if (!reportServiceRef.current) {
|
|
499
|
+
reportServiceRef.current = ReportService.getInstance();
|
|
500
|
+
}
|
|
456
501
|
const exportToPDF = useCallback3(
|
|
457
502
|
async (elementId, options) => {
|
|
503
|
+
if (!reportServiceRef.current) {
|
|
504
|
+
throw new Error("ReportService not initialized");
|
|
505
|
+
}
|
|
458
506
|
setIsExporting(true);
|
|
459
507
|
setError(null);
|
|
460
508
|
try {
|
|
461
|
-
await
|
|
509
|
+
await reportServiceRef.current.exportToPDF(elementId, options);
|
|
462
510
|
} catch (err) {
|
|
463
511
|
const errorMessage = err instanceof Error ? err.message : "PDF export failed";
|
|
464
512
|
setError(errorMessage);
|
|
@@ -467,14 +515,17 @@ function useReportExport() {
|
|
|
467
515
|
setIsExporting(false);
|
|
468
516
|
}
|
|
469
517
|
},
|
|
470
|
-
[
|
|
518
|
+
[]
|
|
471
519
|
);
|
|
472
520
|
const exportCanvasToPDF = useCallback3(
|
|
473
521
|
async (canvas, options) => {
|
|
522
|
+
if (!reportServiceRef.current) {
|
|
523
|
+
throw new Error("ReportService not initialized");
|
|
524
|
+
}
|
|
474
525
|
setIsExporting(true);
|
|
475
526
|
setError(null);
|
|
476
527
|
try {
|
|
477
|
-
await
|
|
528
|
+
await reportServiceRef.current.exportCanvasToPDF(canvas, options);
|
|
478
529
|
} catch (err) {
|
|
479
530
|
const errorMessage = err instanceof Error ? err.message : "Canvas PDF export failed";
|
|
480
531
|
setError(errorMessage);
|
|
@@ -483,14 +534,17 @@ function useReportExport() {
|
|
|
483
534
|
setIsExporting(false);
|
|
484
535
|
}
|
|
485
536
|
},
|
|
486
|
-
[
|
|
537
|
+
[]
|
|
487
538
|
);
|
|
488
539
|
const exportMultipleToPDF = useCallback3(
|
|
489
540
|
async (elementIds, options) => {
|
|
541
|
+
if (!reportServiceRef.current) {
|
|
542
|
+
throw new Error("ReportService not initialized");
|
|
543
|
+
}
|
|
490
544
|
setIsExporting(true);
|
|
491
545
|
setError(null);
|
|
492
546
|
try {
|
|
493
|
-
await
|
|
547
|
+
await reportServiceRef.current.exportMultipleToPDF(elementIds, options);
|
|
494
548
|
} catch (err) {
|
|
495
549
|
const errorMessage = err instanceof Error ? err.message : "Multi-page PDF export failed";
|
|
496
550
|
setError(errorMessage);
|
|
@@ -499,14 +553,17 @@ function useReportExport() {
|
|
|
499
553
|
setIsExporting(false);
|
|
500
554
|
}
|
|
501
555
|
},
|
|
502
|
-
[
|
|
556
|
+
[]
|
|
503
557
|
);
|
|
504
558
|
const exportToExcel = useCallback3(
|
|
505
559
|
async (data, options) => {
|
|
560
|
+
if (!reportServiceRef.current) {
|
|
561
|
+
throw new Error("ReportService not initialized");
|
|
562
|
+
}
|
|
506
563
|
setIsExporting(true);
|
|
507
564
|
setError(null);
|
|
508
565
|
try {
|
|
509
|
-
await
|
|
566
|
+
await reportServiceRef.current.exportToExcel(data, options);
|
|
510
567
|
} catch (err) {
|
|
511
568
|
const errorMessage = err instanceof Error ? err.message : "Excel export failed";
|
|
512
569
|
setError(errorMessage);
|
|
@@ -515,19 +572,22 @@ function useReportExport() {
|
|
|
515
572
|
setIsExporting(false);
|
|
516
573
|
}
|
|
517
574
|
},
|
|
518
|
-
[
|
|
575
|
+
[]
|
|
519
576
|
);
|
|
520
577
|
const exportToCSV = useCallback3(
|
|
521
578
|
(data, filename, delimiter) => {
|
|
579
|
+
if (!reportServiceRef.current) {
|
|
580
|
+
throw new Error("ReportService not initialized");
|
|
581
|
+
}
|
|
522
582
|
try {
|
|
523
|
-
|
|
583
|
+
reportServiceRef.current.exportToCSV(data, filename, delimiter);
|
|
524
584
|
} catch (err) {
|
|
525
585
|
const errorMessage = err instanceof Error ? err.message : "CSV export failed";
|
|
526
586
|
setError(errorMessage);
|
|
527
587
|
throw err;
|
|
528
588
|
}
|
|
529
589
|
},
|
|
530
|
-
[
|
|
590
|
+
[]
|
|
531
591
|
);
|
|
532
592
|
return {
|
|
533
593
|
isExporting,
|