@umituz/web-canvas 1.2.0 → 1.2.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/{chunk-YRC4YPMO.mjs → chunk-7YLVULSN.mjs} +91 -4
- package/dist/domain/index.d.mts +2 -22
- package/dist/domain/index.d.ts +2 -22
- package/dist/index.js +91 -4
- package/dist/index.mjs +1 -1
- package/dist/presentation/index.d.mts +0 -26
- package/dist/presentation/index.d.ts +0 -26
- package/dist/presentation/index.js +91 -4
- package/dist/presentation/index.mjs +1 -1
- package/package.json +1 -1
- package/src/application/index.ts +0 -4
- package/src/application/services/ReportService.ts +0 -22
- package/src/application/services/ScreenshotStudioService.ts +1 -35
- package/src/domain/config/CanvasConfig.ts +0 -16
- package/src/domain/config/DeviceConfig.ts +0 -18
- package/src/domain/config/ScreenshotStudioConfig.ts +0 -29
- package/src/domain/config/index.ts +0 -4
- package/src/domain/index.ts +0 -4
- package/src/domain/types/canvas.types.ts +1 -35
- package/src/domain/types/freehand.types.ts +2 -23
- package/src/domain/types/index.ts +0 -4
- package/src/domain/types/report.types.ts +0 -31
- package/src/domain/types/screenshot.types.ts +1 -65
- package/src/index.ts +0 -12
- package/src/infrastructure/index.ts +0 -4
- package/src/infrastructure/renderers/CanvasRenderer.ts +0 -55
- package/src/infrastructure/renderers/PatternRenderer.ts +0 -30
- package/src/infrastructure/renderers/ScreenshotRenderer.ts +0 -48
- package/src/infrastructure/services/DeviceConfigService.ts +0 -10
- package/src/infrastructure/services/minimalXlsx.ts +0 -11
- package/src/presentation/hooks/useCanvasExport.ts +2 -22
- package/src/presentation/hooks/useFreehandCanvas.ts +102 -40
- package/src/presentation/hooks/useReportExport.ts +0 -12
- package/src/presentation/hooks/useScreenshotStudio.ts +1 -45
- package/src/presentation/index.ts +0 -4
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Device Configuration Service
|
|
3
|
-
* Manages device configurations and dimensions
|
|
4
|
-
*/
|
|
5
1
|
|
|
6
2
|
import { DeviceType, DeviceConfig } from '../../domain/types/screenshot.types';
|
|
7
3
|
import { DEVICE_DIMENSIONS } from '../../domain/config/DeviceConfig';
|
|
8
4
|
|
|
9
|
-
/**
|
|
10
|
-
* Device configuration service
|
|
11
|
-
*/
|
|
12
5
|
export class DeviceConfigService {
|
|
13
6
|
private static instance: DeviceConfigService;
|
|
14
7
|
private devices: Map<DeviceType, DeviceConfig>;
|
|
@@ -119,7 +112,6 @@ export class DeviceConfigService {
|
|
|
119
112
|
}
|
|
120
113
|
|
|
121
114
|
public getAllDevices(): DeviceConfig[] {
|
|
122
|
-
// Use cached value if available
|
|
123
115
|
if (!this.allDevicesCache) {
|
|
124
116
|
this.allDevicesCache = Array.from(this.devices.values());
|
|
125
117
|
}
|
|
@@ -127,7 +119,6 @@ export class DeviceConfigService {
|
|
|
127
119
|
}
|
|
128
120
|
|
|
129
121
|
public getPhones(): DeviceConfig[] {
|
|
130
|
-
// Use cached value if available
|
|
131
122
|
if (!this.phonesCache) {
|
|
132
123
|
this.phonesCache = this.getAllDevices().filter((d) => d.isPhone);
|
|
133
124
|
}
|
|
@@ -135,7 +126,6 @@ export class DeviceConfigService {
|
|
|
135
126
|
}
|
|
136
127
|
|
|
137
128
|
public getTablets(): DeviceConfig[] {
|
|
138
|
-
// Use cached value if available
|
|
139
129
|
if (!this.tabletsCache) {
|
|
140
130
|
this.tabletsCache = this.getAllDevices().filter((d) => d.isTablet);
|
|
141
131
|
}
|
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal XLSX writer — replaces the unmaintained npm `xlsx` (SheetJS)
|
|
3
|
-
* dependency, whose registry build carries open prototype-pollution and
|
|
4
|
-
* ReDoS advisories with no registry fix. The report export only ever
|
|
5
|
-
* writes flat tables (header row + primitive cells), so a hand-rolled
|
|
6
|
-
* SpreadsheetML package on top of jszip covers the need: inline-string
|
|
7
|
-
* and numeric cells, multiple sheets, Excel/LibreOffice/Sheets-compatible.
|
|
8
|
-
*/
|
|
9
1
|
|
|
10
2
|
export interface XlsxSheet {
|
|
11
3
|
name: string;
|
|
12
|
-
/** Rows of primitive cells; the first row is the header. */
|
|
13
4
|
rows: (string | number | boolean | null | undefined)[][];
|
|
14
5
|
}
|
|
15
6
|
|
|
@@ -24,7 +15,6 @@ function escapeXml(value: string): string {
|
|
|
24
15
|
.replace(/'/g, ''');
|
|
25
16
|
}
|
|
26
17
|
|
|
27
|
-
/** Excel sheet names: max 31 chars, no : \ / ? * [ ]. */
|
|
28
18
|
function safeSheetName(name: string, index: number): string {
|
|
29
19
|
const cleaned = name.replace(/[:\\/?*[\]]/g, ' ').slice(0, 31).trim();
|
|
30
20
|
return cleaned.length > 0 ? cleaned : `Sheet${index + 1}`;
|
|
@@ -66,7 +56,6 @@ function sheetXml(rows: XlsxSheet['rows']): string {
|
|
|
66
56
|
return `${XML_HEADER}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${body}</sheetData></worksheet>`;
|
|
67
57
|
}
|
|
68
58
|
|
|
69
|
-
/** Assembles the sheets into a .xlsx zip blob using jszip. */
|
|
70
59
|
export async function writeXlsxBlob(sheets: XlsxSheet[], JSZip: typeof import('jszip')): Promise<Blob> {
|
|
71
60
|
const zip = new JSZip();
|
|
72
61
|
const safeNames = sheets.map((sheet, index) => safeSheetName(sheet.name, index));
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useCanvasExport Hook
|
|
3
|
-
* React hook for canvas export operations
|
|
4
|
-
*/
|
|
5
1
|
|
|
6
2
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
7
3
|
import type { CanvasExportOptions, CanvasResult } from '../../domain/types/canvas.types';
|
|
@@ -9,16 +5,10 @@ import type { CanvasConfig } from '../../domain/config/CanvasConfig';
|
|
|
9
5
|
import { DEFAULT_CANVAS_CONFIG } from '../../domain/config/CanvasConfig';
|
|
10
6
|
import { CanvasRenderer } from '../../infrastructure/renderers/CanvasRenderer';
|
|
11
7
|
|
|
12
|
-
/**
|
|
13
|
-
* useCanvasExport hook options
|
|
14
|
-
*/
|
|
15
8
|
export interface UseCanvasExportOptions {
|
|
16
9
|
config?: CanvasConfig;
|
|
17
10
|
}
|
|
18
11
|
|
|
19
|
-
/**
|
|
20
|
-
* useCanvasExport hook return
|
|
21
|
-
*/
|
|
22
12
|
export interface UseCanvasExportReturn {
|
|
23
13
|
isExporting: boolean;
|
|
24
14
|
progress: number;
|
|
@@ -40,9 +30,6 @@ export interface UseCanvasExportReturn {
|
|
|
40
30
|
cloneCanvas: (canvas: HTMLCanvasElement) => HTMLCanvasElement | null;
|
|
41
31
|
}
|
|
42
32
|
|
|
43
|
-
/**
|
|
44
|
-
* useCanvasExport hook
|
|
45
|
-
*/
|
|
46
33
|
export function useCanvasExport(
|
|
47
34
|
options: UseCanvasExportOptions = {}
|
|
48
35
|
): UseCanvasExportReturn {
|
|
@@ -52,26 +39,22 @@ export function useCanvasExport(
|
|
|
52
39
|
const [progress, setProgress] = useState(0);
|
|
53
40
|
const [error, setError] = useState<string | null>(null);
|
|
54
41
|
|
|
55
|
-
// Use ref to prevent recreating renderer on every render
|
|
56
42
|
const rendererRef = useRef<CanvasRenderer | null>(null);
|
|
57
43
|
const urlCleanupRef = useRef<string[]>([]);
|
|
58
44
|
|
|
59
|
-
// Initialize renderer once
|
|
60
45
|
if (!rendererRef.current) {
|
|
61
46
|
rendererRef.current = new CanvasRenderer(config);
|
|
62
47
|
}
|
|
63
48
|
|
|
64
|
-
// Cleanup URLs on unmount
|
|
65
49
|
useEffect(() => {
|
|
66
50
|
return () => {
|
|
67
|
-
// Clean up any remaining URLs
|
|
68
51
|
urlCleanupRef.current.forEach(url => {
|
|
69
52
|
try {
|
|
70
53
|
URL.revokeObjectURL(url);
|
|
71
|
-
} catch
|
|
72
|
-
// Ignore errors during cleanup
|
|
54
|
+
} catch {
|
|
73
55
|
}
|
|
74
56
|
});
|
|
57
|
+
|
|
75
58
|
urlCleanupRef.current = [];
|
|
76
59
|
};
|
|
77
60
|
}, []);
|
|
@@ -91,7 +74,6 @@ export function useCanvasExport(
|
|
|
91
74
|
const result = await rendererRef.current.exportToBlob(canvas, options);
|
|
92
75
|
setProgress(100);
|
|
93
76
|
|
|
94
|
-
// Track URL for cleanup
|
|
95
77
|
if (result?.url) {
|
|
96
78
|
urlCleanupRef.current.push(result.url);
|
|
97
79
|
}
|
|
@@ -154,10 +136,8 @@ export function useCanvasExport(
|
|
|
154
136
|
|
|
155
137
|
setProgress(100);
|
|
156
138
|
|
|
157
|
-
// Schedule URL cleanup after a delay
|
|
158
139
|
setTimeout(() => {
|
|
159
140
|
URL.revokeObjectURL(result.url);
|
|
160
|
-
// Remove from cleanup tracking
|
|
161
141
|
const index = urlCleanupRef.current.indexOf(result.url);
|
|
162
142
|
if (index > -1) {
|
|
163
143
|
urlCleanupRef.current.splice(index, 1);
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useFreehandCanvas Hook
|
|
3
|
-
* React hook for pointer-based freehand drawing on a canvas
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
7
2
|
import type {
|
|
8
3
|
FreehandPoint,
|
|
@@ -12,64 +7,53 @@ import type {
|
|
|
12
7
|
|
|
13
8
|
const DEFAULT_BRUSH_SIZE = 3;
|
|
14
9
|
const DEFAULT_COLOR = '#1a1a1a';
|
|
10
|
+
const DEFAULT_KEYBOARD_STEP = 4;
|
|
11
|
+
const KEYBOARD_FAST_FACTOR = 4;
|
|
15
12
|
|
|
16
|
-
/**
|
|
17
|
-
* Width for a point: constant for mouse/touch, pressure-scaled for pens
|
|
18
|
-
*/
|
|
19
13
|
function widthFor(stroke: FreehandStroke, point: FreehandPoint): number {
|
|
20
14
|
if (!stroke.dynamic) return stroke.size;
|
|
21
|
-
// pressure is 0 (some mice) – 1 (hard press); clamp the visual range
|
|
22
15
|
return stroke.size * (0.5 + Math.min(1, Math.max(0, point.pressure)));
|
|
23
16
|
}
|
|
24
17
|
|
|
25
|
-
/**
|
|
26
|
-
* Return value of the useFreehandCanvas hook
|
|
27
|
-
*/
|
|
28
18
|
export interface UseFreehandCanvasReturn {
|
|
29
|
-
/** Ref to attach to the <canvas> element that captures drawing. */
|
|
30
19
|
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
|
31
|
-
/** Remove the most recent stroke. */
|
|
32
20
|
undo: () => void;
|
|
33
|
-
/** Remove all strokes. */
|
|
34
21
|
clear: () => void;
|
|
35
|
-
/** Number of committed strokes. */
|
|
36
22
|
strokeCount: number;
|
|
37
|
-
/** True when no strokes are committed. */
|
|
38
23
|
isEmpty: boolean;
|
|
39
|
-
/** Snapshot of committed strokes (safe to serialize). */
|
|
40
24
|
getStrokes: () => FreehandStroke[];
|
|
41
25
|
}
|
|
42
26
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*
|
|
51
|
-
* The canvas gets `touch-action: none` applied so drawing works on touch
|
|
52
|
-
* screens without scrolling the page. Consumers typically layer the canvas
|
|
53
|
-
* over their own guide content (e.g. a faint glyph to trace).
|
|
54
|
-
*/
|
|
27
|
+
interface KeyboardPen {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
down: boolean;
|
|
31
|
+
engaged: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
55
34
|
export function useFreehandCanvas(
|
|
56
35
|
options: UseFreehandCanvasOptions = {}
|
|
57
36
|
): UseFreehandCanvasReturn {
|
|
58
|
-
const {
|
|
37
|
+
const {
|
|
38
|
+
brushSize = DEFAULT_BRUSH_SIZE,
|
|
39
|
+
color = DEFAULT_COLOR,
|
|
40
|
+
pressureEnabled = true,
|
|
41
|
+
keyboard = false,
|
|
42
|
+
keyboardStep = DEFAULT_KEYBOARD_STEP,
|
|
43
|
+
} = options;
|
|
59
44
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
optionsRef.current = { brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd };
|
|
45
|
+
const optionsRef = useRef({ brushSize, color, pressureEnabled, keyboardStep, onStrokeEnd: options.onStrokeEnd });
|
|
46
|
+
optionsRef.current = { brushSize, color, pressureEnabled, keyboardStep, onStrokeEnd: options.onStrokeEnd };
|
|
63
47
|
|
|
64
48
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
65
49
|
const strokesRef = useRef<FreehandStroke[]>([]);
|
|
66
50
|
const activeStrokeRef = useRef<FreehandStroke | null>(null);
|
|
67
51
|
const activePointerIdRef = useRef<number | null>(null);
|
|
68
52
|
const dprRef = useRef(1);
|
|
53
|
+
const penRef = useRef<KeyboardPen | null>(null);
|
|
69
54
|
|
|
70
55
|
const [strokeCount, setStrokeCount] = useState(0);
|
|
71
56
|
|
|
72
|
-
/** Replay every committed stroke (and the active one) at current DPR. */
|
|
73
57
|
const redraw = useCallback(() => {
|
|
74
58
|
const canvas = canvasRef.current;
|
|
75
59
|
const ctx = canvas?.getContext('2d');
|
|
@@ -103,9 +87,20 @@ export function useFreehandCanvas(
|
|
|
103
87
|
ctx.stroke();
|
|
104
88
|
}
|
|
105
89
|
}
|
|
90
|
+
const pen = penRef.current;
|
|
91
|
+
if (pen?.engaged && !pen.down) {
|
|
92
|
+
const reach = 5;
|
|
93
|
+
ctx.strokeStyle = 'rgba(35, 37, 43, 0.55)';
|
|
94
|
+
ctx.lineWidth = 1;
|
|
95
|
+
ctx.beginPath();
|
|
96
|
+
ctx.moveTo(pen.x - reach, pen.y);
|
|
97
|
+
ctx.lineTo(pen.x + reach, pen.y);
|
|
98
|
+
ctx.moveTo(pen.x, pen.y - reach);
|
|
99
|
+
ctx.lineTo(pen.x, pen.y + reach);
|
|
100
|
+
ctx.stroke();
|
|
101
|
+
}
|
|
106
102
|
}, []);
|
|
107
103
|
|
|
108
|
-
/** Match the backing store to the CSS box; returns true when it changed. */
|
|
109
104
|
const syncDimensions = useCallback((): boolean => {
|
|
110
105
|
const canvas = canvasRef.current;
|
|
111
106
|
if (!canvas) return false;
|
|
@@ -125,8 +120,9 @@ export function useFreehandCanvas(
|
|
|
125
120
|
|
|
126
121
|
const el: HTMLCanvasElement = canvas;
|
|
127
122
|
|
|
128
|
-
// Touch screens must draw, not scroll, while the pointer is down.
|
|
129
123
|
el.style.touchAction = 'none';
|
|
124
|
+
if (keyboard && !el.hasAttribute('tabindex')) el.tabIndex = 0;
|
|
125
|
+
if (!penRef.current) penRef.current = { x: el.clientWidth / 2, y: el.clientHeight / 2, down: false, engaged: false };
|
|
130
126
|
|
|
131
127
|
syncDimensions();
|
|
132
128
|
redraw();
|
|
@@ -169,8 +165,6 @@ export function useFreehandCanvas(
|
|
|
169
165
|
|
|
170
166
|
function onPointerDown(event: PointerEvent): void {
|
|
171
167
|
if (event.button !== 0 && event.pointerType === 'mouse') return;
|
|
172
|
-
// Only one stroke at a time: a resting palm or second finger touching
|
|
173
|
-
// mid-stroke must not hijack or corrupt the active stroke.
|
|
174
168
|
if (activeStrokeRef.current != null) return;
|
|
175
169
|
event.preventDefault();
|
|
176
170
|
el.setPointerCapture(event.pointerId);
|
|
@@ -183,6 +177,7 @@ export function useFreehandCanvas(
|
|
|
183
177
|
points: [pointFrom(event)],
|
|
184
178
|
};
|
|
185
179
|
activeStrokeRef.current = stroke;
|
|
180
|
+
penRef.current = { x: stroke.points[0].x, y: stroke.points[0].y, down: false, engaged: false };
|
|
186
181
|
drawDot(stroke, stroke.points[0]);
|
|
187
182
|
}
|
|
188
183
|
|
|
@@ -209,10 +204,76 @@ export function useFreehandCanvas(
|
|
|
209
204
|
optionsRef.current.onStrokeEnd?.(stroke);
|
|
210
205
|
}
|
|
211
206
|
|
|
207
|
+
function clampPen(pen: KeyboardPen): void {
|
|
208
|
+
pen.x = Math.min(Math.max(pen.x, 0), Math.max(0, el.clientWidth - 1));
|
|
209
|
+
pen.y = Math.min(Math.max(pen.y, 0), Math.max(0, el.clientHeight - 1));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function startKeyboardStroke(pen: KeyboardPen): void {
|
|
213
|
+
const { brushSize: size, color: ink } = optionsRef.current;
|
|
214
|
+
const stroke: FreehandStroke = {
|
|
215
|
+
size,
|
|
216
|
+
color: ink,
|
|
217
|
+
dynamic: false,
|
|
218
|
+
points: [{ x: pen.x, y: pen.y, pressure: 0.5 }],
|
|
219
|
+
};
|
|
220
|
+
activeStrokeRef.current = stroke;
|
|
221
|
+
pen.down = true;
|
|
222
|
+
drawDot(stroke, stroke.points[0]);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function commitKeyboardStroke(pen: KeyboardPen): void {
|
|
226
|
+
const stroke = activeStrokeRef.current;
|
|
227
|
+
if (!stroke) return;
|
|
228
|
+
activeStrokeRef.current = null;
|
|
229
|
+
pen.down = false;
|
|
230
|
+
strokesRef.current = [...strokesRef.current, stroke];
|
|
231
|
+
setStrokeCount(strokesRef.current.length);
|
|
232
|
+
optionsRef.current.onStrokeEnd?.(stroke);
|
|
233
|
+
redraw();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
237
|
+
const pen = penRef.current;
|
|
238
|
+
if (!pen) return;
|
|
239
|
+
const fast = event.shiftKey ? KEYBOARD_FAST_FACTOR : 1;
|
|
240
|
+
const step = optionsRef.current.keyboardStep * fast;
|
|
241
|
+
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight' || event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
|
242
|
+
event.preventDefault();
|
|
243
|
+
pen.engaged = true;
|
|
244
|
+
pen.x += event.key === 'ArrowLeft' ? -step : event.key === 'ArrowRight' ? step : 0;
|
|
245
|
+
pen.y += event.key === 'ArrowUp' ? -step : event.key === 'ArrowDown' ? step : 0;
|
|
246
|
+
clampPen(pen);
|
|
247
|
+
const stroke = activeStrokeRef.current;
|
|
248
|
+
if (pen.down && stroke) {
|
|
249
|
+
const point: FreehandPoint = { x: pen.x, y: pen.y, pressure: 0.5 };
|
|
250
|
+
const previous = stroke.points[stroke.points.length - 1];
|
|
251
|
+
stroke.points.push(point);
|
|
252
|
+
drawSegment(stroke, previous, point);
|
|
253
|
+
} else {
|
|
254
|
+
redraw();
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (event.key === ' ' || event.key === 'Enter') {
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
pen.engaged = true;
|
|
261
|
+
if (pen.down) commitKeyboardStroke(pen);
|
|
262
|
+
else startKeyboardStroke(pen);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (event.key === 'Escape' && pen.down) {
|
|
266
|
+
activeStrokeRef.current = null;
|
|
267
|
+
pen.down = false;
|
|
268
|
+
redraw();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
212
272
|
el.addEventListener('pointerdown', onPointerDown);
|
|
213
273
|
el.addEventListener('pointermove', onPointerMove);
|
|
214
274
|
el.addEventListener('pointerup', commit);
|
|
215
275
|
el.addEventListener('pointercancel', commit);
|
|
276
|
+
if (keyboard) el.addEventListener('keydown', onKeyDown);
|
|
216
277
|
|
|
217
278
|
return () => {
|
|
218
279
|
observer.disconnect();
|
|
@@ -220,8 +281,9 @@ export function useFreehandCanvas(
|
|
|
220
281
|
el.removeEventListener('pointermove', onPointerMove);
|
|
221
282
|
el.removeEventListener('pointerup', commit);
|
|
222
283
|
el.removeEventListener('pointercancel', commit);
|
|
284
|
+
el.removeEventListener('keydown', onKeyDown);
|
|
223
285
|
};
|
|
224
|
-
}, [redraw, syncDimensions]);
|
|
286
|
+
}, [redraw, syncDimensions, keyboard]);
|
|
225
287
|
|
|
226
288
|
const undo = useCallback(() => {
|
|
227
289
|
if (strokesRef.current.length === 0) return;
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useReportExport Hook
|
|
3
|
-
* React hook for report export operations
|
|
4
|
-
*/
|
|
5
1
|
|
|
6
2
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
7
3
|
import type { ReportData } from '../../domain/types/report.types';
|
|
@@ -11,9 +7,6 @@ import type {
|
|
|
11
7
|
} from '../../domain/types/report.types';
|
|
12
8
|
import { ReportService } from '../../application/services/ReportService';
|
|
13
9
|
|
|
14
|
-
/**
|
|
15
|
-
* useReportExport hook return
|
|
16
|
-
*/
|
|
17
10
|
export interface UseReportExportReturn {
|
|
18
11
|
isExporting: boolean;
|
|
19
12
|
error: string | null;
|
|
@@ -40,17 +33,12 @@ export interface UseReportExportReturn {
|
|
|
40
33
|
) => void;
|
|
41
34
|
}
|
|
42
35
|
|
|
43
|
-
/**
|
|
44
|
-
* useReportExport hook
|
|
45
|
-
*/
|
|
46
36
|
export function useReportExport(): UseReportExportReturn {
|
|
47
37
|
const [isExporting, setIsExporting] = useState(false);
|
|
48
38
|
const [error, setError] = useState<string | null>(null);
|
|
49
39
|
|
|
50
|
-
// Use ref to prevent recreating service instance on every render
|
|
51
40
|
const reportServiceRef = useRef<ReportService | null>(null);
|
|
52
41
|
|
|
53
|
-
// Initialize service once
|
|
54
42
|
if (!reportServiceRef.current) {
|
|
55
43
|
reportServiceRef.current = ReportService.getInstance();
|
|
56
44
|
}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useScreenshotStudio Hook
|
|
3
|
-
* React hook for screenshot studio functionality
|
|
4
|
-
*/
|
|
5
1
|
|
|
6
2
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
7
3
|
import type {
|
|
@@ -20,19 +16,12 @@ import type { ScreenshotStudioConfig } from '../../domain/config/ScreenshotStudi
|
|
|
20
16
|
import { DEFAULT_SCREENSHOT_STUDIO_CONFIG } from '../../domain/config/ScreenshotStudioConfig';
|
|
21
17
|
import { ScreenshotStudioService } from '../../application/services/ScreenshotStudioService';
|
|
22
18
|
|
|
23
|
-
/**
|
|
24
|
-
* useScreenshotStudio hook options
|
|
25
|
-
*/
|
|
26
19
|
export interface UseScreenshotStudioOptions {
|
|
27
20
|
config?: Partial<ScreenshotStudioConfig>;
|
|
28
21
|
onShowToast?: (title: string, description?: string) => void;
|
|
29
22
|
}
|
|
30
23
|
|
|
31
|
-
/**
|
|
32
|
-
* useScreenshotStudio hook return
|
|
33
|
-
*/
|
|
34
24
|
export interface UseScreenshotStudioReturn {
|
|
35
|
-
// State
|
|
36
25
|
screens: ScreenshotContent[];
|
|
37
26
|
design: ScreenshotDesign;
|
|
38
27
|
currentLanguage: Language;
|
|
@@ -44,33 +33,26 @@ export interface UseScreenshotStudioReturn {
|
|
|
44
33
|
appIcon: string | null;
|
|
45
34
|
projectId: string | null;
|
|
46
35
|
|
|
47
|
-
// Design management
|
|
48
36
|
updateDesign: (updates: Partial<ScreenshotDesign>) => void;
|
|
49
37
|
updateActiveScreen: (updates: Partial<ScreenshotContent>) => void;
|
|
50
38
|
setSelectedScreenId: (screenId: string) => void;
|
|
51
39
|
|
|
52
|
-
// Language management
|
|
53
40
|
changeLanguage: (newLanguage: Language) => void;
|
|
54
41
|
|
|
55
|
-
// Screen management
|
|
56
42
|
addScreen: () => void;
|
|
57
43
|
removeScreen: (screenId: string) => void;
|
|
58
44
|
duplicateScreen: (screenId: string) => void;
|
|
59
45
|
|
|
60
|
-
// Template management
|
|
61
46
|
applyTemplate: (template: ScreenshotTemplate) => void;
|
|
62
47
|
|
|
63
|
-
// Generation
|
|
64
48
|
generateDesignedScreenshot: (
|
|
65
49
|
screenId: string
|
|
66
50
|
) => Promise<{ url: string; id: string } | undefined>;
|
|
67
51
|
|
|
68
|
-
// Export
|
|
69
52
|
exportAllScreensAsZip: (
|
|
70
53
|
format?: ScreenshotFormat
|
|
71
54
|
) => Promise<void>;
|
|
72
55
|
|
|
73
|
-
// AI features (if available)
|
|
74
56
|
handleAIMagic?: (screenId: string) => Promise<void>;
|
|
75
57
|
handleTranslate?: (
|
|
76
58
|
screenId: string,
|
|
@@ -78,7 +60,6 @@ export interface UseScreenshotStudioReturn {
|
|
|
78
60
|
) => Promise<void>;
|
|
79
61
|
handleGenerateMetadata?: () => Promise<void>;
|
|
80
62
|
|
|
81
|
-
// Project management
|
|
82
63
|
setAppMetadata: (metadata: AppStoreMetadata) => void;
|
|
83
64
|
setAppIcon: (icon: string | null) => void;
|
|
84
65
|
setProjectId: (id: string | null) => void;
|
|
@@ -86,9 +67,6 @@ export interface UseScreenshotStudioReturn {
|
|
|
86
67
|
_loadProjectState: (state: Partial<ScreenshotProject>) => void;
|
|
87
68
|
}
|
|
88
69
|
|
|
89
|
-
/**
|
|
90
|
-
* Default screens
|
|
91
|
-
*/
|
|
92
70
|
const DEFAULT_SCREENS: ScreenshotContent[] = [
|
|
93
71
|
{
|
|
94
72
|
id: '1',
|
|
@@ -132,9 +110,6 @@ const DEFAULT_SCREENS: ScreenshotContent[] = [
|
|
|
132
110
|
},
|
|
133
111
|
];
|
|
134
112
|
|
|
135
|
-
/**
|
|
136
|
-
* Default design
|
|
137
|
-
*/
|
|
138
113
|
const DEFAULT_DESIGN: ScreenshotDesign = {
|
|
139
114
|
backgroundColor: '#6366f1',
|
|
140
115
|
backgroundGradientEnd: '#a855f7',
|
|
@@ -169,9 +144,6 @@ const DEFAULT_DESIGN: ScreenshotDesign = {
|
|
|
169
144
|
isPanoramic: false,
|
|
170
145
|
};
|
|
171
146
|
|
|
172
|
-
/**
|
|
173
|
-
* Default app metadata
|
|
174
|
-
*/
|
|
175
147
|
const DEFAULT_APP_METADATA: AppStoreMetadata = {
|
|
176
148
|
appName: 'Growdo',
|
|
177
149
|
subtitle: 'AI Content Generator',
|
|
@@ -182,9 +154,6 @@ const DEFAULT_APP_METADATA: AppStoreMetadata = {
|
|
|
182
154
|
whatsNew: 'New Design Studio integration!',
|
|
183
155
|
};
|
|
184
156
|
|
|
185
|
-
/**
|
|
186
|
-
* useScreenshotStudio hook
|
|
187
|
-
*/
|
|
188
157
|
export function useScreenshotStudio(
|
|
189
158
|
options: UseScreenshotStudioOptions = {}
|
|
190
159
|
): UseScreenshotStudioReturn {
|
|
@@ -200,21 +169,10 @@ export function useScreenshotStudio(
|
|
|
200
169
|
|
|
201
170
|
const serviceRef = useRef<ScreenshotStudioService | null>(null);
|
|
202
171
|
|
|
203
|
-
// Initialize service once
|
|
204
172
|
if (!serviceRef.current) {
|
|
205
173
|
serviceRef.current = new ScreenshotStudioService(fullConfig);
|
|
206
174
|
}
|
|
207
175
|
|
|
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
|
-
}, []);
|
|
217
|
-
|
|
218
176
|
const [screens, setScreens] = useState<ScreenshotContent[]>(DEFAULT_SCREENS);
|
|
219
177
|
const [design, setDesign] = useState<ScreenshotDesign>(DEFAULT_DESIGN);
|
|
220
178
|
const [currentLanguage, setCurrentLanguage] = useState<Language>(
|
|
@@ -414,13 +372,11 @@ export function useScreenshotStudio(
|
|
|
414
372
|
} finally {
|
|
415
373
|
setIsProcessing(false);
|
|
416
374
|
|
|
417
|
-
// Clean up object URL after a delay
|
|
418
375
|
if (url) {
|
|
419
376
|
setTimeout(() => {
|
|
420
377
|
try {
|
|
421
378
|
URL.revokeObjectURL(url!);
|
|
422
|
-
} catch
|
|
423
|
-
// Ignore cleanup errors
|
|
379
|
+
} catch {
|
|
424
380
|
}
|
|
425
381
|
}, 100);
|
|
426
382
|
}
|