@umituz/web-canvas 1.0.1 → 1.0.3
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/LICENSE +21 -0
- package/README.md +32 -0
- 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 +14 -3
- 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
|
@@ -81,6 +81,7 @@ var DEFAULT_CANVAS_CONFIG = {
|
|
|
81
81
|
var CanvasRenderer = class {
|
|
82
82
|
constructor(config = DEFAULT_CANVAS_CONFIG) {
|
|
83
83
|
this.config = { ...DEFAULT_CANVAS_CONFIG, ...config };
|
|
84
|
+
this.imageCache = /* @__PURE__ */ new Map();
|
|
84
85
|
}
|
|
85
86
|
/**
|
|
86
87
|
* Create a new canvas element
|
|
@@ -182,17 +183,37 @@ var CanvasRenderer = class {
|
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
185
|
/**
|
|
185
|
-
* Load image from URL
|
|
186
|
+
* Load image from URL with caching
|
|
186
187
|
*/
|
|
187
188
|
loadImage(url) {
|
|
189
|
+
if (this.imageCache.has(url)) {
|
|
190
|
+
return Promise.resolve(this.imageCache.get(url));
|
|
191
|
+
}
|
|
188
192
|
return new Promise((resolve, reject) => {
|
|
189
193
|
const img = new Image();
|
|
190
|
-
|
|
191
|
-
|
|
194
|
+
if (this.config.useCORS) {
|
|
195
|
+
img.crossOrigin = "anonymous";
|
|
196
|
+
}
|
|
197
|
+
img.onload = () => {
|
|
198
|
+
this.imageCache.set(url, img);
|
|
199
|
+
resolve(img);
|
|
200
|
+
};
|
|
192
201
|
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
|
|
193
202
|
img.src = url;
|
|
194
203
|
});
|
|
195
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Clear image cache to free memory
|
|
207
|
+
*/
|
|
208
|
+
clearImageCache() {
|
|
209
|
+
this.imageCache.clear();
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Get cache size for monitoring
|
|
213
|
+
*/
|
|
214
|
+
getCacheSize() {
|
|
215
|
+
return this.imageCache.size;
|
|
216
|
+
}
|
|
196
217
|
/**
|
|
197
218
|
* Create gradient
|
|
198
219
|
*/
|
|
@@ -274,10 +295,15 @@ var CanvasRenderer = class {
|
|
|
274
295
|
|
|
275
296
|
// src/infrastructure/renderers/PatternRenderer.ts
|
|
276
297
|
var PatternRenderer = class {
|
|
298
|
+
constructor() {
|
|
299
|
+
this.patternCanvasCache = /* @__PURE__ */ new Map();
|
|
300
|
+
this.MAX_CACHE_SIZE = 10;
|
|
301
|
+
}
|
|
277
302
|
/**
|
|
278
303
|
* Render pattern on canvas
|
|
279
304
|
*/
|
|
280
305
|
renderPattern(ctx, pattern, dimensions, color = "#000000") {
|
|
306
|
+
if (pattern === "none") return;
|
|
281
307
|
ctx.save();
|
|
282
308
|
ctx.globalAlpha = 0.1;
|
|
283
309
|
switch (pattern) {
|
|
@@ -296,62 +322,65 @@ var PatternRenderer = class {
|
|
|
296
322
|
case "checkers":
|
|
297
323
|
this.renderCheckerPattern(ctx, dimensions, color);
|
|
298
324
|
break;
|
|
299
|
-
case "none":
|
|
300
|
-
default:
|
|
301
|
-
break;
|
|
302
325
|
}
|
|
303
326
|
ctx.restore();
|
|
304
327
|
}
|
|
305
328
|
/**
|
|
306
|
-
*
|
|
329
|
+
* Clear pattern cache to free memory
|
|
330
|
+
*/
|
|
331
|
+
clearCache() {
|
|
332
|
+
this.patternCanvasCache.clear();
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Render dot pattern - optimized with batch rendering
|
|
307
336
|
*/
|
|
308
337
|
renderDotPattern(ctx, dimensions, color) {
|
|
309
338
|
ctx.fillStyle = color;
|
|
310
339
|
const spacing = 30;
|
|
340
|
+
const radius = 2;
|
|
341
|
+
ctx.beginPath();
|
|
311
342
|
for (let x = spacing; x < dimensions.width; x += spacing) {
|
|
312
343
|
for (let y = spacing; y < dimensions.height; y += spacing) {
|
|
313
|
-
ctx.
|
|
314
|
-
ctx.arc(x, y,
|
|
315
|
-
ctx.fill();
|
|
344
|
+
ctx.moveTo(x + radius, y);
|
|
345
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
316
346
|
}
|
|
317
347
|
}
|
|
348
|
+
ctx.fill();
|
|
318
349
|
}
|
|
319
350
|
/**
|
|
320
|
-
* Render grid pattern
|
|
351
|
+
* Render grid pattern - optimized with single path
|
|
321
352
|
*/
|
|
322
353
|
renderGridPattern(ctx, dimensions, color) {
|
|
323
354
|
ctx.strokeStyle = color;
|
|
324
355
|
ctx.lineWidth = 1;
|
|
325
356
|
const spacing = 30;
|
|
357
|
+
ctx.beginPath();
|
|
326
358
|
for (let x = 0; x <= dimensions.width; x += spacing) {
|
|
327
|
-
ctx.beginPath();
|
|
328
359
|
ctx.moveTo(x, 0);
|
|
329
360
|
ctx.lineTo(x, dimensions.height);
|
|
330
|
-
ctx.stroke();
|
|
331
361
|
}
|
|
332
362
|
for (let y = 0; y <= dimensions.height; y += spacing) {
|
|
333
|
-
ctx.beginPath();
|
|
334
363
|
ctx.moveTo(0, y);
|
|
335
364
|
ctx.lineTo(dimensions.width, y);
|
|
336
|
-
ctx.stroke();
|
|
337
365
|
}
|
|
366
|
+
ctx.stroke();
|
|
338
367
|
}
|
|
339
368
|
/**
|
|
340
|
-
* Render line pattern
|
|
369
|
+
* Render line pattern - optimized with single path
|
|
341
370
|
*/
|
|
342
371
|
renderLinePattern(ctx, dimensions, color) {
|
|
343
372
|
ctx.strokeStyle = color;
|
|
344
373
|
ctx.lineWidth = 2;
|
|
345
374
|
const spacing = 20;
|
|
375
|
+
ctx.beginPath();
|
|
346
376
|
for (let y = spacing; y < dimensions.height; y += spacing) {
|
|
347
|
-
ctx.beginPath();
|
|
348
377
|
ctx.moveTo(0, y);
|
|
349
378
|
ctx.lineTo(dimensions.width, y);
|
|
350
|
-
ctx.stroke();
|
|
351
379
|
}
|
|
380
|
+
ctx.stroke();
|
|
352
381
|
}
|
|
353
382
|
/**
|
|
354
|
-
* Render wave pattern
|
|
383
|
+
* Render wave pattern - optimized with pre-calculated values
|
|
355
384
|
*/
|
|
356
385
|
renderWavePattern(ctx, dimensions, color) {
|
|
357
386
|
ctx.strokeStyle = color;
|
|
@@ -359,15 +388,15 @@ var PatternRenderer = class {
|
|
|
359
388
|
const amplitude = 10;
|
|
360
389
|
const frequency = 0.1;
|
|
361
390
|
const spacing = 40;
|
|
391
|
+
const step = 5;
|
|
362
392
|
for (let y = spacing; y < dimensions.height; y += spacing) {
|
|
363
393
|
ctx.beginPath();
|
|
364
|
-
|
|
394
|
+
const startX = 0;
|
|
395
|
+
const startY = y + Math.sin(startX * frequency) * amplitude;
|
|
396
|
+
ctx.moveTo(startX, startY);
|
|
397
|
+
for (let x = step; x <= dimensions.width; x += step) {
|
|
365
398
|
const waveY = y + Math.sin(x * frequency) * amplitude;
|
|
366
|
-
|
|
367
|
-
ctx.moveTo(x, waveY);
|
|
368
|
-
} else {
|
|
369
|
-
ctx.lineTo(x, waveY);
|
|
370
|
-
}
|
|
399
|
+
ctx.lineTo(x, waveY);
|
|
371
400
|
}
|
|
372
401
|
ctx.stroke();
|
|
373
402
|
}
|
|
@@ -409,6 +438,9 @@ var DEFAULT_DEVICE_CONFIG = {
|
|
|
409
438
|
// src/infrastructure/services/DeviceConfigService.ts
|
|
410
439
|
var DeviceConfigService = class _DeviceConfigService {
|
|
411
440
|
constructor() {
|
|
441
|
+
this.phonesCache = null;
|
|
442
|
+
this.tabletsCache = null;
|
|
443
|
+
this.allDevicesCache = null;
|
|
412
444
|
this.devices = /* @__PURE__ */ new Map();
|
|
413
445
|
this.initializeDevices();
|
|
414
446
|
}
|
|
@@ -505,13 +537,22 @@ var DeviceConfigService = class _DeviceConfigService {
|
|
|
505
537
|
return this.devices.get(deviceType);
|
|
506
538
|
}
|
|
507
539
|
getAllDevices() {
|
|
508
|
-
|
|
540
|
+
if (!this.allDevicesCache) {
|
|
541
|
+
this.allDevicesCache = Array.from(this.devices.values());
|
|
542
|
+
}
|
|
543
|
+
return this.allDevicesCache;
|
|
509
544
|
}
|
|
510
545
|
getPhones() {
|
|
511
|
-
|
|
546
|
+
if (!this.phonesCache) {
|
|
547
|
+
this.phonesCache = this.getAllDevices().filter((d) => d.isPhone);
|
|
548
|
+
}
|
|
549
|
+
return this.phonesCache;
|
|
512
550
|
}
|
|
513
551
|
getTablets() {
|
|
514
|
-
|
|
552
|
+
if (!this.tabletsCache) {
|
|
553
|
+
this.tabletsCache = this.getAllDevices().filter((d) => d.isTablet);
|
|
554
|
+
}
|
|
555
|
+
return this.tabletsCache;
|
|
515
556
|
}
|
|
516
557
|
isPhone(deviceType) {
|
|
517
558
|
const config = this.getDeviceConfig(deviceType);
|
|
@@ -530,10 +571,20 @@ var DeviceConfigService = class _DeviceConfigService {
|
|
|
530
571
|
// src/infrastructure/renderers/ScreenshotRenderer.ts
|
|
531
572
|
var ScreenshotRenderer = class {
|
|
532
573
|
constructor() {
|
|
574
|
+
this.renderCache = /* @__PURE__ */ new Map();
|
|
575
|
+
this.MAX_CACHE_SIZE = 5;
|
|
533
576
|
this.canvasRenderer = new CanvasRenderer();
|
|
534
577
|
this.patternRenderer = new PatternRenderer();
|
|
535
578
|
this.deviceConfigService = DeviceConfigService.getInstance();
|
|
536
579
|
}
|
|
580
|
+
/**
|
|
581
|
+
* Clear render cache to free memory
|
|
582
|
+
*/
|
|
583
|
+
clearCache() {
|
|
584
|
+
this.renderCache.clear();
|
|
585
|
+
this.canvasRenderer.clearImageCache?.();
|
|
586
|
+
this.patternRenderer.clearCache?.();
|
|
587
|
+
}
|
|
537
588
|
/**
|
|
538
589
|
* Render device frame
|
|
539
590
|
*/
|
|
@@ -585,7 +636,7 @@ var ScreenshotRenderer = class {
|
|
|
585
636
|
ctx.fill();
|
|
586
637
|
}
|
|
587
638
|
/**
|
|
588
|
-
* Render background
|
|
639
|
+
* Render background - optimized
|
|
589
640
|
*/
|
|
590
641
|
renderBackground(ctx, design, dimensions) {
|
|
591
642
|
if (design.backgroundGradientEnd) {
|
|
@@ -795,10 +846,17 @@ var ScreenshotStudioService = class {
|
|
|
795
846
|
* Generate batch screenshots
|
|
796
847
|
*/
|
|
797
848
|
async generateBatchScreenshots(deviceTypes) {
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
)
|
|
801
|
-
|
|
849
|
+
const BATCH_SIZE = 3;
|
|
850
|
+
const results = [];
|
|
851
|
+
for (let i = 0; i < deviceTypes.length; i += BATCH_SIZE) {
|
|
852
|
+
const batch = deviceTypes.slice(i, i + BATCH_SIZE);
|
|
853
|
+
const batchPromises = batch.map(
|
|
854
|
+
(deviceType) => this.generateScreenshot({ deviceType })
|
|
855
|
+
);
|
|
856
|
+
const batchResults = await Promise.all(batchPromises);
|
|
857
|
+
results.push(...batchResults);
|
|
858
|
+
}
|
|
859
|
+
return results;
|
|
802
860
|
}
|
|
803
861
|
/**
|
|
804
862
|
* Generate designed screenshot
|
|
@@ -839,16 +897,18 @@ WHATS NEW:
|
|
|
839
897
|
${project.appMetadata.whatsNew}`;
|
|
840
898
|
folder.file("aso_metadata.txt", metadataText);
|
|
841
899
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
900
|
+
const screenshotPromises = project.screens.map(
|
|
901
|
+
(screen, index) => this.generateDesignedScreenshot(screen, project.design).then((screenshot) => ({ screenshot, index }))
|
|
902
|
+
);
|
|
903
|
+
const screenshots = await Promise.all(screenshotPromises);
|
|
904
|
+
screenshots.forEach(({ screenshot, index }) => {
|
|
845
905
|
const base64Data = screenshot.url.split(",")[1];
|
|
846
906
|
folder.file(
|
|
847
|
-
`screen_${
|
|
907
|
+
`screen_${index + 1}_${project.design.deviceType}.png`,
|
|
848
908
|
base64Data,
|
|
849
909
|
{ base64: true }
|
|
850
910
|
);
|
|
851
|
-
}
|
|
911
|
+
});
|
|
852
912
|
return await zip.generateAsync({ type: "blob" });
|
|
853
913
|
}
|
|
854
914
|
/**
|
|
@@ -932,10 +992,13 @@ ${project.appMetadata.whatsNew}`;
|
|
|
932
992
|
};
|
|
933
993
|
}
|
|
934
994
|
/**
|
|
935
|
-
* Generate unique ID
|
|
995
|
+
* Generate unique ID - optimized version
|
|
936
996
|
*/
|
|
937
997
|
generateId() {
|
|
938
|
-
|
|
998
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
999
|
+
return crypto.randomUUID();
|
|
1000
|
+
}
|
|
1001
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 9)}`;
|
|
939
1002
|
}
|
|
940
1003
|
};
|
|
941
1004
|
|
|
@@ -1033,9 +1096,16 @@ function useScreenshotStudio(options = {}) {
|
|
|
1033
1096
|
...DEFAULT_SCREENSHOT_STUDIO_CONFIG,
|
|
1034
1097
|
...config
|
|
1035
1098
|
};
|
|
1036
|
-
const serviceRef = (0, import_react.useRef)(
|
|
1037
|
-
|
|
1038
|
-
|
|
1099
|
+
const serviceRef = (0, import_react.useRef)(null);
|
|
1100
|
+
if (!serviceRef.current) {
|
|
1101
|
+
serviceRef.current = new ScreenshotStudioService(fullConfig);
|
|
1102
|
+
}
|
|
1103
|
+
(0, import_react.useEffect)(() => {
|
|
1104
|
+
return () => {
|
|
1105
|
+
if (serviceRef.current) {
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
}, []);
|
|
1039
1109
|
const [screens, setScreens] = (0, import_react.useState)(DEFAULT_SCREENS);
|
|
1040
1110
|
const [design, setDesign] = (0, import_react.useState)(DEFAULT_DESIGN);
|
|
1041
1111
|
const [currentLanguage, setCurrentLanguage] = (0, import_react.useState)(
|
|
@@ -1156,6 +1226,7 @@ function useScreenshotStudio(options = {}) {
|
|
|
1156
1226
|
);
|
|
1157
1227
|
const generateDesignedScreenshot = (0, import_react.useCallback)(
|
|
1158
1228
|
async (screenId) => {
|
|
1229
|
+
if (!serviceRef.current) return;
|
|
1159
1230
|
const screen = screens.find((s) => s.id === screenId);
|
|
1160
1231
|
if (!screen) return;
|
|
1161
1232
|
setIsProcessing(true);
|
|
@@ -1176,7 +1247,9 @@ function useScreenshotStudio(options = {}) {
|
|
|
1176
1247
|
);
|
|
1177
1248
|
const exportAllScreensAsZip = (0, import_react.useCallback)(
|
|
1178
1249
|
async (format = "png") => {
|
|
1250
|
+
if (!serviceRef.current) return;
|
|
1179
1251
|
setIsProcessing(true);
|
|
1252
|
+
let url = null;
|
|
1180
1253
|
try {
|
|
1181
1254
|
const project = {
|
|
1182
1255
|
id: projectId || "default-project",
|
|
@@ -1191,12 +1264,11 @@ function useScreenshotStudio(options = {}) {
|
|
|
1191
1264
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1192
1265
|
};
|
|
1193
1266
|
const blob = await serviceRef.current.exportProjectAsZip(project);
|
|
1194
|
-
|
|
1267
|
+
url = URL.createObjectURL(blob);
|
|
1195
1268
|
const link = document.createElement("a");
|
|
1196
1269
|
link.href = url;
|
|
1197
1270
|
link.download = `Studio_${currentLanguage}_${Date.now()}.zip`;
|
|
1198
1271
|
link.click();
|
|
1199
|
-
URL.revokeObjectURL(url);
|
|
1200
1272
|
onShowToast(
|
|
1201
1273
|
"Success",
|
|
1202
1274
|
`Exported ${screens.length} screens in ZIP archive.`
|
|
@@ -1205,6 +1277,14 @@ function useScreenshotStudio(options = {}) {
|
|
|
1205
1277
|
onShowToast("Error", "ZIP export failed");
|
|
1206
1278
|
} finally {
|
|
1207
1279
|
setIsProcessing(false);
|
|
1280
|
+
if (url) {
|
|
1281
|
+
setTimeout(() => {
|
|
1282
|
+
try {
|
|
1283
|
+
URL.revokeObjectURL(url);
|
|
1284
|
+
} catch (e) {
|
|
1285
|
+
}
|
|
1286
|
+
}, 100);
|
|
1287
|
+
}
|
|
1208
1288
|
}
|
|
1209
1289
|
},
|
|
1210
1290
|
[
|
|
@@ -1290,15 +1370,34 @@ function useCanvasExport(options = {}) {
|
|
|
1290
1370
|
const [isExporting, setIsExporting] = (0, import_react2.useState)(false);
|
|
1291
1371
|
const [progress, setProgress] = (0, import_react2.useState)(0);
|
|
1292
1372
|
const [error, setError] = (0, import_react2.useState)(null);
|
|
1293
|
-
const
|
|
1373
|
+
const rendererRef = (0, import_react2.useRef)(null);
|
|
1374
|
+
const urlCleanupRef = (0, import_react2.useRef)([]);
|
|
1375
|
+
if (!rendererRef.current) {
|
|
1376
|
+
rendererRef.current = new CanvasRenderer(config);
|
|
1377
|
+
}
|
|
1378
|
+
(0, import_react2.useEffect)(() => {
|
|
1379
|
+
return () => {
|
|
1380
|
+
urlCleanupRef.current.forEach((url) => {
|
|
1381
|
+
try {
|
|
1382
|
+
URL.revokeObjectURL(url);
|
|
1383
|
+
} catch (e) {
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
urlCleanupRef.current = [];
|
|
1387
|
+
};
|
|
1388
|
+
}, []);
|
|
1294
1389
|
const exportToBlob = (0, import_react2.useCallback)(
|
|
1295
1390
|
async (canvas, options2) => {
|
|
1391
|
+
if (!rendererRef.current) return null;
|
|
1296
1392
|
setIsExporting(true);
|
|
1297
1393
|
setProgress(0);
|
|
1298
1394
|
setError(null);
|
|
1299
1395
|
try {
|
|
1300
|
-
const result = await
|
|
1396
|
+
const result = await rendererRef.current.exportToBlob(canvas, options2);
|
|
1301
1397
|
setProgress(100);
|
|
1398
|
+
if (result?.url) {
|
|
1399
|
+
urlCleanupRef.current.push(result.url);
|
|
1400
|
+
}
|
|
1302
1401
|
return result;
|
|
1303
1402
|
} catch (err) {
|
|
1304
1403
|
const errorMessage = err instanceof Error ? err.message : "Export failed";
|
|
@@ -1308,19 +1407,20 @@ function useCanvasExport(options = {}) {
|
|
|
1308
1407
|
setIsExporting(false);
|
|
1309
1408
|
}
|
|
1310
1409
|
},
|
|
1311
|
-
[
|
|
1410
|
+
[]
|
|
1312
1411
|
);
|
|
1313
1412
|
const exportToDataURL = (0, import_react2.useCallback)(
|
|
1314
1413
|
(canvas, format, quality) => {
|
|
1414
|
+
if (!rendererRef.current) return null;
|
|
1315
1415
|
try {
|
|
1316
|
-
return
|
|
1416
|
+
return rendererRef.current.exportToDataURL(canvas, format, quality);
|
|
1317
1417
|
} catch (err) {
|
|
1318
1418
|
const errorMessage = err instanceof Error ? err.message : "Export failed";
|
|
1319
1419
|
setError(errorMessage);
|
|
1320
1420
|
return null;
|
|
1321
1421
|
}
|
|
1322
1422
|
},
|
|
1323
|
-
[
|
|
1423
|
+
[]
|
|
1324
1424
|
);
|
|
1325
1425
|
const downloadCanvas = (0, import_react2.useCallback)(
|
|
1326
1426
|
async (canvas, filename, options2) => {
|
|
@@ -1340,6 +1440,10 @@ function useCanvasExport(options = {}) {
|
|
|
1340
1440
|
setProgress(100);
|
|
1341
1441
|
setTimeout(() => {
|
|
1342
1442
|
URL.revokeObjectURL(result.url);
|
|
1443
|
+
const index = urlCleanupRef.current.indexOf(result.url);
|
|
1444
|
+
if (index > -1) {
|
|
1445
|
+
urlCleanupRef.current.splice(index, 1);
|
|
1446
|
+
}
|
|
1343
1447
|
}, 100);
|
|
1344
1448
|
} catch (err) {
|
|
1345
1449
|
const errorMessage = err instanceof Error ? err.message : "Download failed";
|
|
@@ -1352,15 +1456,16 @@ function useCanvasExport(options = {}) {
|
|
|
1352
1456
|
);
|
|
1353
1457
|
const cloneCanvas = (0, import_react2.useCallback)(
|
|
1354
1458
|
(canvas) => {
|
|
1459
|
+
if (!rendererRef.current) return null;
|
|
1355
1460
|
try {
|
|
1356
|
-
return
|
|
1461
|
+
return rendererRef.current.cloneCanvas(canvas);
|
|
1357
1462
|
} catch (err) {
|
|
1358
1463
|
const errorMessage = err instanceof Error ? err.message : "Clone failed";
|
|
1359
1464
|
setError(errorMessage);
|
|
1360
1465
|
return null;
|
|
1361
1466
|
}
|
|
1362
1467
|
},
|
|
1363
|
-
[
|
|
1468
|
+
[]
|
|
1364
1469
|
);
|
|
1365
1470
|
return {
|
|
1366
1471
|
isExporting,
|
|
@@ -1522,13 +1627,19 @@ var ReportService = class _ReportService {
|
|
|
1522
1627
|
function useReportExport() {
|
|
1523
1628
|
const [isExporting, setIsExporting] = (0, import_react3.useState)(false);
|
|
1524
1629
|
const [error, setError] = (0, import_react3.useState)(null);
|
|
1525
|
-
const
|
|
1630
|
+
const reportServiceRef = (0, import_react3.useRef)(null);
|
|
1631
|
+
if (!reportServiceRef.current) {
|
|
1632
|
+
reportServiceRef.current = ReportService.getInstance();
|
|
1633
|
+
}
|
|
1526
1634
|
const exportToPDF = (0, import_react3.useCallback)(
|
|
1527
1635
|
async (elementId, options) => {
|
|
1636
|
+
if (!reportServiceRef.current) {
|
|
1637
|
+
throw new Error("ReportService not initialized");
|
|
1638
|
+
}
|
|
1528
1639
|
setIsExporting(true);
|
|
1529
1640
|
setError(null);
|
|
1530
1641
|
try {
|
|
1531
|
-
await
|
|
1642
|
+
await reportServiceRef.current.exportToPDF(elementId, options);
|
|
1532
1643
|
} catch (err) {
|
|
1533
1644
|
const errorMessage = err instanceof Error ? err.message : "PDF export failed";
|
|
1534
1645
|
setError(errorMessage);
|
|
@@ -1537,14 +1648,17 @@ function useReportExport() {
|
|
|
1537
1648
|
setIsExporting(false);
|
|
1538
1649
|
}
|
|
1539
1650
|
},
|
|
1540
|
-
[
|
|
1651
|
+
[]
|
|
1541
1652
|
);
|
|
1542
1653
|
const exportCanvasToPDF = (0, import_react3.useCallback)(
|
|
1543
1654
|
async (canvas, options) => {
|
|
1655
|
+
if (!reportServiceRef.current) {
|
|
1656
|
+
throw new Error("ReportService not initialized");
|
|
1657
|
+
}
|
|
1544
1658
|
setIsExporting(true);
|
|
1545
1659
|
setError(null);
|
|
1546
1660
|
try {
|
|
1547
|
-
await
|
|
1661
|
+
await reportServiceRef.current.exportCanvasToPDF(canvas, options);
|
|
1548
1662
|
} catch (err) {
|
|
1549
1663
|
const errorMessage = err instanceof Error ? err.message : "Canvas PDF export failed";
|
|
1550
1664
|
setError(errorMessage);
|
|
@@ -1553,14 +1667,17 @@ function useReportExport() {
|
|
|
1553
1667
|
setIsExporting(false);
|
|
1554
1668
|
}
|
|
1555
1669
|
},
|
|
1556
|
-
[
|
|
1670
|
+
[]
|
|
1557
1671
|
);
|
|
1558
1672
|
const exportMultipleToPDF = (0, import_react3.useCallback)(
|
|
1559
1673
|
async (elementIds, options) => {
|
|
1674
|
+
if (!reportServiceRef.current) {
|
|
1675
|
+
throw new Error("ReportService not initialized");
|
|
1676
|
+
}
|
|
1560
1677
|
setIsExporting(true);
|
|
1561
1678
|
setError(null);
|
|
1562
1679
|
try {
|
|
1563
|
-
await
|
|
1680
|
+
await reportServiceRef.current.exportMultipleToPDF(elementIds, options);
|
|
1564
1681
|
} catch (err) {
|
|
1565
1682
|
const errorMessage = err instanceof Error ? err.message : "Multi-page PDF export failed";
|
|
1566
1683
|
setError(errorMessage);
|
|
@@ -1569,14 +1686,17 @@ function useReportExport() {
|
|
|
1569
1686
|
setIsExporting(false);
|
|
1570
1687
|
}
|
|
1571
1688
|
},
|
|
1572
|
-
[
|
|
1689
|
+
[]
|
|
1573
1690
|
);
|
|
1574
1691
|
const exportToExcel = (0, import_react3.useCallback)(
|
|
1575
1692
|
async (data, options) => {
|
|
1693
|
+
if (!reportServiceRef.current) {
|
|
1694
|
+
throw new Error("ReportService not initialized");
|
|
1695
|
+
}
|
|
1576
1696
|
setIsExporting(true);
|
|
1577
1697
|
setError(null);
|
|
1578
1698
|
try {
|
|
1579
|
-
await
|
|
1699
|
+
await reportServiceRef.current.exportToExcel(data, options);
|
|
1580
1700
|
} catch (err) {
|
|
1581
1701
|
const errorMessage = err instanceof Error ? err.message : "Excel export failed";
|
|
1582
1702
|
setError(errorMessage);
|
|
@@ -1585,19 +1705,22 @@ function useReportExport() {
|
|
|
1585
1705
|
setIsExporting(false);
|
|
1586
1706
|
}
|
|
1587
1707
|
},
|
|
1588
|
-
[
|
|
1708
|
+
[]
|
|
1589
1709
|
);
|
|
1590
1710
|
const exportToCSV = (0, import_react3.useCallback)(
|
|
1591
1711
|
(data, filename, delimiter) => {
|
|
1712
|
+
if (!reportServiceRef.current) {
|
|
1713
|
+
throw new Error("ReportService not initialized");
|
|
1714
|
+
}
|
|
1592
1715
|
try {
|
|
1593
|
-
|
|
1716
|
+
reportServiceRef.current.exportToCSV(data, filename, delimiter);
|
|
1594
1717
|
} catch (err) {
|
|
1595
1718
|
const errorMessage = err instanceof Error ? err.message : "CSV export failed";
|
|
1596
1719
|
setError(errorMessage);
|
|
1597
1720
|
throw err;
|
|
1598
1721
|
}
|
|
1599
1722
|
},
|
|
1600
|
-
[
|
|
1723
|
+
[]
|
|
1601
1724
|
);
|
|
1602
1725
|
return {
|
|
1603
1726
|
isExporting,
|
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
useCanvasExport,
|
|
3
3
|
useReportExport,
|
|
4
4
|
useScreenshotStudio
|
|
5
|
-
} from "../chunk-
|
|
6
|
-
import "../chunk-
|
|
7
|
-
import "../chunk-
|
|
5
|
+
} from "../chunk-LOVI2LTO.mjs";
|
|
6
|
+
import "../chunk-DNKRSL6D.mjs";
|
|
7
|
+
import "../chunk-UOGTEMIJ.mjs";
|
|
8
8
|
import "../chunk-Z6IB4BQV.mjs";
|
|
9
9
|
import "../chunk-MC6PNV2F.mjs";
|
|
10
10
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-canvas",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Comprehensive canvas and screenshot generation library with device frames, patterns, and PDF export",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsup src/index.ts src/domain/index.ts src/application/index.ts src/presentation/index.ts src/infrastructure/index.ts src/domain/config/index.ts --format cjs,esm --dts --clean --external react",
|
|
50
50
|
"dev": "tsup src/index.ts src/domain/index.ts src/application/index.ts src/presentation/index.ts src/infrastructure/index.ts src/domain/config/index.ts --format cjs,esm --dts --watch --external react",
|
|
51
|
-
"lint": "
|
|
51
|
+
"lint": "eslint ."
|
|
52
52
|
},
|
|
53
53
|
"keywords": [
|
|
54
54
|
"canvas",
|
|
@@ -69,13 +69,24 @@
|
|
|
69
69
|
],
|
|
70
70
|
"author": "umituz",
|
|
71
71
|
"license": "MIT",
|
|
72
|
+
"repository": {
|
|
73
|
+
"type": "git",
|
|
74
|
+
"url": "https://github.com/umituz/web-canvas"
|
|
75
|
+
},
|
|
76
|
+
"bugs": {
|
|
77
|
+
"url": "https://github.com/umituz/web-canvas/issues"
|
|
78
|
+
},
|
|
79
|
+
"homepage": "https://github.com/umituz/web-canvas#readme",
|
|
72
80
|
"peerDependencies": {
|
|
73
81
|
"react": ">=18"
|
|
74
82
|
},
|
|
75
83
|
"devDependencies": {
|
|
84
|
+
"@eslint/js": "^9.32.0",
|
|
76
85
|
"@types/react": "^18.2.0",
|
|
86
|
+
"eslint": "^9.32.0",
|
|
77
87
|
"tsup": "^8.0.0",
|
|
78
|
-
"typescript": "^5.0.0"
|
|
88
|
+
"typescript": "^5.0.0",
|
|
89
|
+
"typescript-eslint": "^8.38.0"
|
|
79
90
|
},
|
|
80
91
|
"dependencies": {
|
|
81
92
|
"html2canvas": "^1.4.1",
|
|
@@ -79,10 +79,21 @@ export class ScreenshotStudioService {
|
|
|
79
79
|
public async generateBatchScreenshots(
|
|
80
80
|
deviceTypes: DeviceType[]
|
|
81
81
|
): Promise<ScreenshotMetadata[]> {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
// Process in parallel with concurrency control for better performance
|
|
83
|
+
const BATCH_SIZE = 3; // Process 3 screenshots at a time to avoid memory issues
|
|
84
|
+
|
|
85
|
+
const results: ScreenshotMetadata[] = [];
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < deviceTypes.length; i += BATCH_SIZE) {
|
|
88
|
+
const batch = deviceTypes.slice(i, i + BATCH_SIZE);
|
|
89
|
+
const batchPromises = batch.map((deviceType) =>
|
|
90
|
+
this.generateScreenshot({ deviceType })
|
|
91
|
+
);
|
|
92
|
+
const batchResults = await Promise.all(batchPromises);
|
|
93
|
+
results.push(...batchResults);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return results;
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
/**
|
|
@@ -136,17 +147,23 @@ ${project.appMetadata.whatsNew}`;
|
|
|
136
147
|
folder.file('aso_metadata.txt', metadataText);
|
|
137
148
|
}
|
|
138
149
|
|
|
139
|
-
// Generate screenshots
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
// Generate screenshots in parallel for better performance
|
|
151
|
+
const screenshotPromises = project.screens.map((screen, index) =>
|
|
152
|
+
this.generateDesignedScreenshot(screen, project.design)
|
|
153
|
+
.then(screenshot => ({ screenshot, index }))
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const screenshots = await Promise.all(screenshotPromises);
|
|
157
|
+
|
|
158
|
+
// Add all screenshots to ZIP
|
|
159
|
+
screenshots.forEach(({ screenshot, index }) => {
|
|
143
160
|
const base64Data = screenshot.url.split(',')[1];
|
|
144
161
|
folder.file(
|
|
145
|
-
`screen_${
|
|
162
|
+
`screen_${index + 1}_${project.design.deviceType}.png`,
|
|
146
163
|
base64Data,
|
|
147
164
|
{ base64: true }
|
|
148
165
|
);
|
|
149
|
-
}
|
|
166
|
+
});
|
|
150
167
|
|
|
151
168
|
return await zip.generateAsync({ type: 'blob' });
|
|
152
169
|
}
|
|
@@ -235,9 +252,15 @@ ${project.appMetadata.whatsNew}`;
|
|
|
235
252
|
}
|
|
236
253
|
|
|
237
254
|
/**
|
|
238
|
-
* Generate unique ID
|
|
255
|
+
* Generate unique ID - optimized version
|
|
239
256
|
*/
|
|
240
257
|
private generateId(): string {
|
|
241
|
-
|
|
258
|
+
// Use more efficient ID generation with crypto if available
|
|
259
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
260
|
+
return crypto.randomUUID();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Fallback to timestamp + random for better uniqueness
|
|
264
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 9)}`;
|
|
242
265
|
}
|
|
243
266
|
}
|