@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
|
@@ -13,9 +13,11 @@ import { DEFAULT_CANVAS_CONFIG } from '../../domain/config/CanvasConfig';
|
|
|
13
13
|
|
|
14
14
|
export class CanvasRenderer {
|
|
15
15
|
private config: typeof DEFAULT_CANVAS_CONFIG;
|
|
16
|
+
private imageCache: Map<string, HTMLImageElement>;
|
|
16
17
|
|
|
17
18
|
constructor(config = DEFAULT_CANVAS_CONFIG) {
|
|
18
19
|
this.config = { ...DEFAULT_CANVAS_CONFIG, ...config };
|
|
20
|
+
this.imageCache = new Map();
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -164,18 +166,43 @@ export class CanvasRenderer {
|
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
/**
|
|
167
|
-
* Load image from URL
|
|
169
|
+
* Load image from URL with caching
|
|
168
170
|
*/
|
|
169
171
|
private loadImage(url: string): Promise<HTMLImageElement> {
|
|
172
|
+
// Check cache first
|
|
173
|
+
if (this.imageCache.has(url)) {
|
|
174
|
+
return Promise.resolve(this.imageCache.get(url)!);
|
|
175
|
+
}
|
|
176
|
+
|
|
170
177
|
return new Promise((resolve, reject) => {
|
|
171
178
|
const img = new Image();
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
if (this.config.useCORS) {
|
|
180
|
+
img.crossOrigin = 'anonymous';
|
|
181
|
+
}
|
|
182
|
+
img.onload = () => {
|
|
183
|
+
// Cache the loaded image
|
|
184
|
+
this.imageCache.set(url, img);
|
|
185
|
+
resolve(img);
|
|
186
|
+
};
|
|
174
187
|
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
|
|
175
188
|
img.src = url;
|
|
176
189
|
});
|
|
177
190
|
}
|
|
178
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Clear image cache to free memory
|
|
194
|
+
*/
|
|
195
|
+
public clearImageCache(): void {
|
|
196
|
+
this.imageCache.clear();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get cache size for monitoring
|
|
201
|
+
*/
|
|
202
|
+
public getCacheSize(): number {
|
|
203
|
+
return this.imageCache.size;
|
|
204
|
+
}
|
|
205
|
+
|
|
179
206
|
/**
|
|
180
207
|
* Create gradient
|
|
181
208
|
*/
|
|
@@ -7,6 +7,9 @@ import type { CanvasDimensions } from '../../domain/types/canvas.types';
|
|
|
7
7
|
import type { BackgroundPattern } from '../../domain/types/screenshot.types';
|
|
8
8
|
|
|
9
9
|
export class PatternRenderer {
|
|
10
|
+
private patternCanvasCache: Map<string, HTMLCanvasElement> = new Map();
|
|
11
|
+
private readonly MAX_CACHE_SIZE = 10;
|
|
12
|
+
|
|
10
13
|
/**
|
|
11
14
|
* Render pattern on canvas
|
|
12
15
|
*/
|
|
@@ -16,6 +19,8 @@ export class PatternRenderer {
|
|
|
16
19
|
dimensions: CanvasDimensions,
|
|
17
20
|
color: string = '#000000'
|
|
18
21
|
): void {
|
|
22
|
+
if (pattern === 'none') return;
|
|
23
|
+
|
|
19
24
|
ctx.save();
|
|
20
25
|
ctx.globalAlpha = 0.1;
|
|
21
26
|
|
|
@@ -35,17 +40,20 @@ export class PatternRenderer {
|
|
|
35
40
|
case 'checkers':
|
|
36
41
|
this.renderCheckerPattern(ctx, dimensions, color);
|
|
37
42
|
break;
|
|
38
|
-
case 'none':
|
|
39
|
-
default:
|
|
40
|
-
// Do nothing
|
|
41
|
-
break;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
ctx.restore();
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
|
-
*
|
|
49
|
+
* Clear pattern cache to free memory
|
|
50
|
+
*/
|
|
51
|
+
public clearCache(): void {
|
|
52
|
+
this.patternCanvasCache.clear();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Render dot pattern - optimized with batch rendering
|
|
49
57
|
*/
|
|
50
58
|
private renderDotPattern(
|
|
51
59
|
ctx: CanvasRenderingContext2D,
|
|
@@ -54,18 +62,21 @@ export class PatternRenderer {
|
|
|
54
62
|
): void {
|
|
55
63
|
ctx.fillStyle = color;
|
|
56
64
|
const spacing = 30;
|
|
65
|
+
const radius = 2;
|
|
57
66
|
|
|
67
|
+
// Batch rendering for better performance
|
|
68
|
+
ctx.beginPath();
|
|
58
69
|
for (let x = spacing; x < dimensions.width; x += spacing) {
|
|
59
70
|
for (let y = spacing; y < dimensions.height; y += spacing) {
|
|
60
|
-
ctx.
|
|
61
|
-
ctx.arc(x, y,
|
|
62
|
-
ctx.fill();
|
|
71
|
+
ctx.moveTo(x + radius, y);
|
|
72
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
63
73
|
}
|
|
64
74
|
}
|
|
75
|
+
ctx.fill();
|
|
65
76
|
}
|
|
66
77
|
|
|
67
78
|
/**
|
|
68
|
-
* Render grid pattern
|
|
79
|
+
* Render grid pattern - optimized with single path
|
|
69
80
|
*/
|
|
70
81
|
private renderGridPattern(
|
|
71
82
|
ctx: CanvasRenderingContext2D,
|
|
@@ -76,23 +87,25 @@ export class PatternRenderer {
|
|
|
76
87
|
ctx.lineWidth = 1;
|
|
77
88
|
const spacing = 30;
|
|
78
89
|
|
|
90
|
+
ctx.beginPath();
|
|
91
|
+
|
|
92
|
+
// Vertical lines
|
|
79
93
|
for (let x = 0; x <= dimensions.width; x += spacing) {
|
|
80
|
-
ctx.beginPath();
|
|
81
94
|
ctx.moveTo(x, 0);
|
|
82
95
|
ctx.lineTo(x, dimensions.height);
|
|
83
|
-
ctx.stroke();
|
|
84
96
|
}
|
|
85
97
|
|
|
98
|
+
// Horizontal lines
|
|
86
99
|
for (let y = 0; y <= dimensions.height; y += spacing) {
|
|
87
|
-
ctx.beginPath();
|
|
88
100
|
ctx.moveTo(0, y);
|
|
89
101
|
ctx.lineTo(dimensions.width, y);
|
|
90
|
-
ctx.stroke();
|
|
91
102
|
}
|
|
103
|
+
|
|
104
|
+
ctx.stroke();
|
|
92
105
|
}
|
|
93
106
|
|
|
94
107
|
/**
|
|
95
|
-
* Render line pattern
|
|
108
|
+
* Render line pattern - optimized with single path
|
|
96
109
|
*/
|
|
97
110
|
private renderLinePattern(
|
|
98
111
|
ctx: CanvasRenderingContext2D,
|
|
@@ -103,16 +116,19 @@ export class PatternRenderer {
|
|
|
103
116
|
ctx.lineWidth = 2;
|
|
104
117
|
const spacing = 20;
|
|
105
118
|
|
|
119
|
+
ctx.beginPath();
|
|
120
|
+
|
|
121
|
+
// Draw all lines in a single path
|
|
106
122
|
for (let y = spacing; y < dimensions.height; y += spacing) {
|
|
107
|
-
ctx.beginPath();
|
|
108
123
|
ctx.moveTo(0, y);
|
|
109
124
|
ctx.lineTo(dimensions.width, y);
|
|
110
|
-
ctx.stroke();
|
|
111
125
|
}
|
|
126
|
+
|
|
127
|
+
ctx.stroke();
|
|
112
128
|
}
|
|
113
129
|
|
|
114
130
|
/**
|
|
115
|
-
* Render wave pattern
|
|
131
|
+
* Render wave pattern - optimized with pre-calculated values
|
|
116
132
|
*/
|
|
117
133
|
private renderWavePattern(
|
|
118
134
|
ctx: CanvasRenderingContext2D,
|
|
@@ -124,17 +140,21 @@ export class PatternRenderer {
|
|
|
124
140
|
const amplitude = 10;
|
|
125
141
|
const frequency = 0.1;
|
|
126
142
|
const spacing = 40;
|
|
143
|
+
const step = 5;
|
|
127
144
|
|
|
128
145
|
for (let y = spacing; y < dimensions.height; y += spacing) {
|
|
129
146
|
ctx.beginPath();
|
|
130
|
-
|
|
147
|
+
|
|
148
|
+
// Pre-calculate wave points and use more efficient path drawing
|
|
149
|
+
const startX = 0;
|
|
150
|
+
const startY = y + Math.sin(startX * frequency) * amplitude;
|
|
151
|
+
ctx.moveTo(startX, startY);
|
|
152
|
+
|
|
153
|
+
for (let x = step; x <= dimensions.width; x += step) {
|
|
131
154
|
const waveY = y + Math.sin(x * frequency) * amplitude;
|
|
132
|
-
|
|
133
|
-
ctx.moveTo(x, waveY);
|
|
134
|
-
} else {
|
|
135
|
-
ctx.lineTo(x, waveY);
|
|
136
|
-
}
|
|
155
|
+
ctx.lineTo(x, waveY);
|
|
137
156
|
}
|
|
157
|
+
|
|
138
158
|
ctx.stroke();
|
|
139
159
|
}
|
|
140
160
|
}
|
|
@@ -22,6 +22,8 @@ export class ScreenshotRenderer {
|
|
|
22
22
|
private canvasRenderer: CanvasRenderer;
|
|
23
23
|
private patternRenderer: PatternRenderer;
|
|
24
24
|
private deviceConfigService: DeviceConfigService;
|
|
25
|
+
private renderCache: Map<string, HTMLCanvasElement> = new Map();
|
|
26
|
+
private readonly MAX_CACHE_SIZE = 5;
|
|
25
27
|
|
|
26
28
|
constructor() {
|
|
27
29
|
this.canvasRenderer = new CanvasRenderer();
|
|
@@ -29,6 +31,15 @@ export class ScreenshotRenderer {
|
|
|
29
31
|
this.deviceConfigService = DeviceConfigService.getInstance();
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Clear render cache to free memory
|
|
36
|
+
*/
|
|
37
|
+
public clearCache(): void {
|
|
38
|
+
this.renderCache.clear();
|
|
39
|
+
this.canvasRenderer.clearImageCache?.();
|
|
40
|
+
this.patternRenderer.clearCache?.();
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
/**
|
|
33
44
|
* Render device frame
|
|
34
45
|
*/
|
|
@@ -108,13 +119,14 @@ export class ScreenshotRenderer {
|
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
/**
|
|
111
|
-
* Render background
|
|
122
|
+
* Render background - optimized
|
|
112
123
|
*/
|
|
113
124
|
public renderBackground(
|
|
114
125
|
ctx: CanvasRenderingContext2D,
|
|
115
126
|
design: ScreenshotDesign,
|
|
116
127
|
dimensions: CanvasDimensions
|
|
117
128
|
): void {
|
|
129
|
+
// Fill background
|
|
118
130
|
if (design.backgroundGradientEnd) {
|
|
119
131
|
const gradient = this.canvasRenderer.createLinearGradient(
|
|
120
132
|
ctx,
|
|
@@ -134,6 +146,7 @@ export class ScreenshotRenderer {
|
|
|
134
146
|
|
|
135
147
|
ctx.fillRect(0, 0, dimensions.width, dimensions.height);
|
|
136
148
|
|
|
149
|
+
// Render pattern if specified (and not none)
|
|
137
150
|
if (design.backgroundPattern && design.backgroundPattern !== 'none') {
|
|
138
151
|
this.patternRenderer.renderPattern(
|
|
139
152
|
ctx,
|
|
@@ -12,6 +12,9 @@ import { DEVICE_DIMENSIONS } from '../../domain/config/DeviceConfig';
|
|
|
12
12
|
export class DeviceConfigService {
|
|
13
13
|
private static instance: DeviceConfigService;
|
|
14
14
|
private devices: Map<DeviceType, DeviceConfig>;
|
|
15
|
+
private phonesCache: DeviceConfig[] | null = null;
|
|
16
|
+
private tabletsCache: DeviceConfig[] | null = null;
|
|
17
|
+
private allDevicesCache: DeviceConfig[] | null = null;
|
|
15
18
|
|
|
16
19
|
private constructor() {
|
|
17
20
|
this.devices = new Map();
|
|
@@ -116,15 +119,27 @@ export class DeviceConfigService {
|
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
public getAllDevices(): DeviceConfig[] {
|
|
119
|
-
|
|
122
|
+
// Use cached value if available
|
|
123
|
+
if (!this.allDevicesCache) {
|
|
124
|
+
this.allDevicesCache = Array.from(this.devices.values());
|
|
125
|
+
}
|
|
126
|
+
return this.allDevicesCache;
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
public getPhones(): DeviceConfig[] {
|
|
123
|
-
|
|
130
|
+
// Use cached value if available
|
|
131
|
+
if (!this.phonesCache) {
|
|
132
|
+
this.phonesCache = this.getAllDevices().filter((d) => d.isPhone);
|
|
133
|
+
}
|
|
134
|
+
return this.phonesCache;
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
public getTablets(): DeviceConfig[] {
|
|
127
|
-
|
|
138
|
+
// Use cached value if available
|
|
139
|
+
if (!this.tabletsCache) {
|
|
140
|
+
this.tabletsCache = this.getAllDevices().filter((d) => d.isTablet);
|
|
141
|
+
}
|
|
142
|
+
return this.tabletsCache;
|
|
128
143
|
}
|
|
129
144
|
|
|
130
145
|
public isPhone(deviceType: DeviceType): boolean {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* React hook for canvas export operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useCallback } from 'react';
|
|
6
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
7
7
|
import type { CanvasExportOptions, CanvasResult } from '../../domain/types/canvas.types';
|
|
8
8
|
import type { CanvasConfig } from '../../domain/config/CanvasConfig';
|
|
9
9
|
import { DEFAULT_CANVAS_CONFIG } from '../../domain/config/CanvasConfig';
|
|
@@ -52,20 +52,50 @@ export function useCanvasExport(
|
|
|
52
52
|
const [progress, setProgress] = useState(0);
|
|
53
53
|
const [error, setError] = useState<string | null>(null);
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
// Use ref to prevent recreating renderer on every render
|
|
56
|
+
const rendererRef = useRef<CanvasRenderer | null>(null);
|
|
57
|
+
const urlCleanupRef = useRef<string[]>([]);
|
|
58
|
+
|
|
59
|
+
// Initialize renderer once
|
|
60
|
+
if (!rendererRef.current) {
|
|
61
|
+
rendererRef.current = new CanvasRenderer(config);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Cleanup URLs on unmount
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
return () => {
|
|
67
|
+
// Clean up any remaining URLs
|
|
68
|
+
urlCleanupRef.current.forEach(url => {
|
|
69
|
+
try {
|
|
70
|
+
URL.revokeObjectURL(url);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// Ignore errors during cleanup
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
urlCleanupRef.current = [];
|
|
76
|
+
};
|
|
77
|
+
}, []);
|
|
56
78
|
|
|
57
79
|
const exportToBlob = useCallback(
|
|
58
80
|
async (
|
|
59
81
|
canvas: HTMLCanvasElement,
|
|
60
82
|
options?: CanvasExportOptions
|
|
61
83
|
): Promise<CanvasResult | null> => {
|
|
84
|
+
if (!rendererRef.current) return null;
|
|
85
|
+
|
|
62
86
|
setIsExporting(true);
|
|
63
87
|
setProgress(0);
|
|
64
88
|
setError(null);
|
|
65
89
|
|
|
66
90
|
try {
|
|
67
|
-
const result = await
|
|
91
|
+
const result = await rendererRef.current.exportToBlob(canvas, options);
|
|
68
92
|
setProgress(100);
|
|
93
|
+
|
|
94
|
+
// Track URL for cleanup
|
|
95
|
+
if (result?.url) {
|
|
96
|
+
urlCleanupRef.current.push(result.url);
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
return result;
|
|
70
100
|
} catch (err) {
|
|
71
101
|
const errorMessage =
|
|
@@ -76,7 +106,7 @@ export function useCanvasExport(
|
|
|
76
106
|
setIsExporting(false);
|
|
77
107
|
}
|
|
78
108
|
},
|
|
79
|
-
[
|
|
109
|
+
[]
|
|
80
110
|
);
|
|
81
111
|
|
|
82
112
|
const exportToDataURL = useCallback(
|
|
@@ -85,8 +115,10 @@ export function useCanvasExport(
|
|
|
85
115
|
format?: string,
|
|
86
116
|
quality?: number
|
|
87
117
|
): string | null => {
|
|
118
|
+
if (!rendererRef.current) return null;
|
|
119
|
+
|
|
88
120
|
try {
|
|
89
|
-
return
|
|
121
|
+
return rendererRef.current.exportToDataURL(canvas, format, quality);
|
|
90
122
|
} catch (err) {
|
|
91
123
|
const errorMessage =
|
|
92
124
|
err instanceof Error ? err.message : 'Export failed';
|
|
@@ -94,7 +126,7 @@ export function useCanvasExport(
|
|
|
94
126
|
return null;
|
|
95
127
|
}
|
|
96
128
|
},
|
|
97
|
-
[
|
|
129
|
+
[]
|
|
98
130
|
);
|
|
99
131
|
|
|
100
132
|
const downloadCanvas = useCallback(
|
|
@@ -122,8 +154,14 @@ export function useCanvasExport(
|
|
|
122
154
|
|
|
123
155
|
setProgress(100);
|
|
124
156
|
|
|
157
|
+
// Schedule URL cleanup after a delay
|
|
125
158
|
setTimeout(() => {
|
|
126
159
|
URL.revokeObjectURL(result.url);
|
|
160
|
+
// Remove from cleanup tracking
|
|
161
|
+
const index = urlCleanupRef.current.indexOf(result.url);
|
|
162
|
+
if (index > -1) {
|
|
163
|
+
urlCleanupRef.current.splice(index, 1);
|
|
164
|
+
}
|
|
127
165
|
}, 100);
|
|
128
166
|
} catch (err) {
|
|
129
167
|
const errorMessage =
|
|
@@ -138,8 +176,10 @@ export function useCanvasExport(
|
|
|
138
176
|
|
|
139
177
|
const cloneCanvas = useCallback(
|
|
140
178
|
(canvas: HTMLCanvasElement): HTMLCanvasElement | null => {
|
|
179
|
+
if (!rendererRef.current) return null;
|
|
180
|
+
|
|
141
181
|
try {
|
|
142
|
-
return
|
|
182
|
+
return rendererRef.current.cloneCanvas(canvas);
|
|
143
183
|
} catch (err) {
|
|
144
184
|
const errorMessage =
|
|
145
185
|
err instanceof Error ? err.message : 'Clone failed';
|
|
@@ -147,7 +187,7 @@ export function useCanvasExport(
|
|
|
147
187
|
return null;
|
|
148
188
|
}
|
|
149
189
|
},
|
|
150
|
-
[
|
|
190
|
+
[]
|
|
151
191
|
);
|
|
152
192
|
|
|
153
193
|
return {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* React hook for report export operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useCallback } from 'react';
|
|
6
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
7
7
|
import type { ReportData } from '../../domain/types/report.types';
|
|
8
8
|
import type {
|
|
9
9
|
PDFExportOptions,
|
|
@@ -47,15 +47,25 @@ export function useReportExport(): UseReportExportReturn {
|
|
|
47
47
|
const [isExporting, setIsExporting] = useState(false);
|
|
48
48
|
const [error, setError] = useState<string | null>(null);
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
// Use ref to prevent recreating service instance on every render
|
|
51
|
+
const reportServiceRef = useRef<ReportService | null>(null);
|
|
52
|
+
|
|
53
|
+
// Initialize service once
|
|
54
|
+
if (!reportServiceRef.current) {
|
|
55
|
+
reportServiceRef.current = ReportService.getInstance();
|
|
56
|
+
}
|
|
51
57
|
|
|
52
58
|
const exportToPDF = useCallback(
|
|
53
59
|
async (elementId: string, options?: PDFExportOptions): Promise<void> => {
|
|
60
|
+
if (!reportServiceRef.current) {
|
|
61
|
+
throw new Error('ReportService not initialized');
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
setIsExporting(true);
|
|
55
65
|
setError(null);
|
|
56
66
|
|
|
57
67
|
try {
|
|
58
|
-
await
|
|
68
|
+
await reportServiceRef.current.exportToPDF(elementId, options);
|
|
59
69
|
} catch (err) {
|
|
60
70
|
const errorMessage =
|
|
61
71
|
err instanceof Error ? err.message : 'PDF export failed';
|
|
@@ -65,7 +75,7 @@ export function useReportExport(): UseReportExportReturn {
|
|
|
65
75
|
setIsExporting(false);
|
|
66
76
|
}
|
|
67
77
|
},
|
|
68
|
-
[
|
|
78
|
+
[]
|
|
69
79
|
);
|
|
70
80
|
|
|
71
81
|
const exportCanvasToPDF = useCallback(
|
|
@@ -73,11 +83,15 @@ export function useReportExport(): UseReportExportReturn {
|
|
|
73
83
|
canvas: HTMLCanvasElement,
|
|
74
84
|
options?: PDFExportOptions
|
|
75
85
|
): Promise<void> => {
|
|
86
|
+
if (!reportServiceRef.current) {
|
|
87
|
+
throw new Error('ReportService not initialized');
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
setIsExporting(true);
|
|
77
91
|
setError(null);
|
|
78
92
|
|
|
79
93
|
try {
|
|
80
|
-
await
|
|
94
|
+
await reportServiceRef.current.exportCanvasToPDF(canvas, options);
|
|
81
95
|
} catch (err) {
|
|
82
96
|
const errorMessage =
|
|
83
97
|
err instanceof Error ? err.message : 'Canvas PDF export failed';
|
|
@@ -87,7 +101,7 @@ export function useReportExport(): UseReportExportReturn {
|
|
|
87
101
|
setIsExporting(false);
|
|
88
102
|
}
|
|
89
103
|
},
|
|
90
|
-
[
|
|
104
|
+
[]
|
|
91
105
|
);
|
|
92
106
|
|
|
93
107
|
const exportMultipleToPDF = useCallback(
|
|
@@ -95,11 +109,15 @@ export function useReportExport(): UseReportExportReturn {
|
|
|
95
109
|
elementIds: string[],
|
|
96
110
|
options?: PDFExportOptions
|
|
97
111
|
): Promise<void> => {
|
|
112
|
+
if (!reportServiceRef.current) {
|
|
113
|
+
throw new Error('ReportService not initialized');
|
|
114
|
+
}
|
|
115
|
+
|
|
98
116
|
setIsExporting(true);
|
|
99
117
|
setError(null);
|
|
100
118
|
|
|
101
119
|
try {
|
|
102
|
-
await
|
|
120
|
+
await reportServiceRef.current.exportMultipleToPDF(elementIds, options);
|
|
103
121
|
} catch (err) {
|
|
104
122
|
const errorMessage =
|
|
105
123
|
err instanceof Error ? err.message : 'Multi-page PDF export failed';
|
|
@@ -109,16 +127,20 @@ export function useReportExport(): UseReportExportReturn {
|
|
|
109
127
|
setIsExporting(false);
|
|
110
128
|
}
|
|
111
129
|
},
|
|
112
|
-
[
|
|
130
|
+
[]
|
|
113
131
|
);
|
|
114
132
|
|
|
115
133
|
const exportToExcel = useCallback(
|
|
116
134
|
async (data: ReportData, options?: ExcelExportOptions): Promise<void> => {
|
|
135
|
+
if (!reportServiceRef.current) {
|
|
136
|
+
throw new Error('ReportService not initialized');
|
|
137
|
+
}
|
|
138
|
+
|
|
117
139
|
setIsExporting(true);
|
|
118
140
|
setError(null);
|
|
119
141
|
|
|
120
142
|
try {
|
|
121
|
-
await
|
|
143
|
+
await reportServiceRef.current.exportToExcel(data, options);
|
|
122
144
|
} catch (err) {
|
|
123
145
|
const errorMessage =
|
|
124
146
|
err instanceof Error ? err.message : 'Excel export failed';
|
|
@@ -128,7 +150,7 @@ export function useReportExport(): UseReportExportReturn {
|
|
|
128
150
|
setIsExporting(false);
|
|
129
151
|
}
|
|
130
152
|
},
|
|
131
|
-
[
|
|
153
|
+
[]
|
|
132
154
|
);
|
|
133
155
|
|
|
134
156
|
const exportToCSV = useCallback(
|
|
@@ -137,8 +159,12 @@ export function useReportExport(): UseReportExportReturn {
|
|
|
137
159
|
filename?: string,
|
|
138
160
|
delimiter?: ',' | ';'
|
|
139
161
|
): void => {
|
|
162
|
+
if (!reportServiceRef.current) {
|
|
163
|
+
throw new Error('ReportService not initialized');
|
|
164
|
+
}
|
|
165
|
+
|
|
140
166
|
try {
|
|
141
|
-
|
|
167
|
+
reportServiceRef.current.exportToCSV(data, filename, delimiter);
|
|
142
168
|
} catch (err) {
|
|
143
169
|
const errorMessage =
|
|
144
170
|
err instanceof Error ? err.message : 'CSV export failed';
|
|
@@ -146,7 +172,7 @@ export function useReportExport(): UseReportExportReturn {
|
|
|
146
172
|
throw err;
|
|
147
173
|
}
|
|
148
174
|
},
|
|
149
|
-
[
|
|
175
|
+
[]
|
|
150
176
|
);
|
|
151
177
|
|
|
152
178
|
return {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* React hook for screenshot studio functionality
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useCallback, useRef } from 'react';
|
|
6
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
7
7
|
import type {
|
|
8
8
|
ScreenshotContent,
|
|
9
9
|
ScreenshotDesign,
|
|
@@ -198,9 +198,22 @@ export function useScreenshotStudio(
|
|
|
198
198
|
...config,
|
|
199
199
|
};
|
|
200
200
|
|
|
201
|
-
const serviceRef = useRef(
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
const serviceRef = useRef<ScreenshotStudioService | null>(null);
|
|
202
|
+
|
|
203
|
+
// Initialize service once
|
|
204
|
+
if (!serviceRef.current) {
|
|
205
|
+
serviceRef.current = new ScreenshotStudioService(fullConfig);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Cleanup on unmount
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
return () => {
|
|
211
|
+
// Clean up service resources if needed
|
|
212
|
+
if (serviceRef.current) {
|
|
213
|
+
// Any cleanup logic can go here
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}, []);
|
|
204
217
|
|
|
205
218
|
const [screens, setScreens] = useState<ScreenshotContent[]>(DEFAULT_SCREENS);
|
|
206
219
|
const [design, setDesign] = useState<ScreenshotDesign>(DEFAULT_DESIGN);
|
|
@@ -342,6 +355,8 @@ export function useScreenshotStudio(
|
|
|
342
355
|
|
|
343
356
|
const generateDesignedScreenshot = useCallback(
|
|
344
357
|
async (screenId: string) => {
|
|
358
|
+
if (!serviceRef.current) return;
|
|
359
|
+
|
|
345
360
|
const screen = screens.find((s) => s.id === screenId);
|
|
346
361
|
if (!screen) return;
|
|
347
362
|
|
|
@@ -364,7 +379,11 @@ export function useScreenshotStudio(
|
|
|
364
379
|
|
|
365
380
|
const exportAllScreensAsZip = useCallback(
|
|
366
381
|
async (format: ScreenshotFormat = 'png') => {
|
|
382
|
+
if (!serviceRef.current) return;
|
|
383
|
+
|
|
367
384
|
setIsProcessing(true);
|
|
385
|
+
let url: string | null = null;
|
|
386
|
+
|
|
368
387
|
try {
|
|
369
388
|
const project: ScreenshotProject = {
|
|
370
389
|
id: projectId || 'default-project',
|
|
@@ -380,12 +399,11 @@ export function useScreenshotStudio(
|
|
|
380
399
|
};
|
|
381
400
|
|
|
382
401
|
const blob = await serviceRef.current.exportProjectAsZip(project);
|
|
383
|
-
|
|
402
|
+
url = URL.createObjectURL(blob);
|
|
384
403
|
const link = document.createElement('a');
|
|
385
404
|
link.href = url;
|
|
386
405
|
link.download = `Studio_${currentLanguage}_${Date.now()}.zip`;
|
|
387
406
|
link.click();
|
|
388
|
-
URL.revokeObjectURL(url);
|
|
389
407
|
|
|
390
408
|
onShowToast(
|
|
391
409
|
'Success',
|
|
@@ -395,6 +413,17 @@ export function useScreenshotStudio(
|
|
|
395
413
|
onShowToast('Error', 'ZIP export failed');
|
|
396
414
|
} finally {
|
|
397
415
|
setIsProcessing(false);
|
|
416
|
+
|
|
417
|
+
// Clean up object URL after a delay
|
|
418
|
+
if (url) {
|
|
419
|
+
setTimeout(() => {
|
|
420
|
+
try {
|
|
421
|
+
URL.revokeObjectURL(url!);
|
|
422
|
+
} catch (e) {
|
|
423
|
+
// Ignore cleanup errors
|
|
424
|
+
}
|
|
425
|
+
}, 100);
|
|
426
|
+
}
|
|
398
427
|
}
|
|
399
428
|
},
|
|
400
429
|
[
|