@umituz/web-canvas 1.0.6 → 1.1.0
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/README.md +33 -0
- package/dist/CanvasConfig-HFgH7irb.d.mts +310 -0
- package/dist/CanvasConfig-HFgH7irb.d.ts +310 -0
- package/dist/application/index.d.mts +78 -0
- package/dist/application/index.d.ts +78 -0
- package/dist/application/index.js +1146 -0
- package/dist/application/index.mjs +11 -0
- package/dist/chunk-DNKRSL6D.mjs +358 -0
- package/dist/chunk-HJ24NSQA.mjs +777 -0
- package/dist/chunk-MC6PNV2F.mjs +34 -0
- package/dist/chunk-TZGTTYTZ.mjs +0 -0
- package/dist/chunk-UOGTEMIJ.mjs +712 -0
- package/dist/chunk-Z6IB4BQV.mjs +31 -0
- package/dist/domain/config/index.d.mts +87 -0
- package/dist/domain/config/index.d.ts +87 -0
- package/dist/domain/config/index.js +92 -0
- package/dist/domain/config/index.mjs +15 -0
- package/dist/domain/index.d.mts +45 -0
- package/dist/domain/index.d.ts +45 -0
- package/dist/domain/index.js +137 -0
- package/dist/domain/index.mjs +55 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1927 -0
- package/dist/index.mjs +40 -0
- package/dist/infrastructure/index.d.mts +198 -0
- package/dist/infrastructure/index.d.ts +198 -0
- package/dist/infrastructure/index.js +765 -0
- package/dist/infrastructure/index.mjs +13 -0
- package/dist/presentation/index.d.mts +145 -0
- package/dist/presentation/index.d.ts +145 -0
- package/dist/presentation/index.js +1911 -0
- package/dist/presentation/index.mjs +16 -0
- package/dist/report.types-DWDnprBx.d.mts +76 -0
- package/dist/report.types-DWDnprBx.d.ts +76 -0
- package/package.json +8 -2
- package/src/domain/types/freehand.types.ts +42 -0
- package/src/domain/types/index.ts +1 -0
- package/src/index.ts +10 -0
- package/src/presentation/hooks/useFreehandCanvas.ts +244 -0
- package/src/presentation/index.ts +1 -0
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReportService,
|
|
3
|
+
ScreenshotStudioService
|
|
4
|
+
} from "./chunk-DNKRSL6D.mjs";
|
|
5
|
+
import {
|
|
6
|
+
CanvasRenderer
|
|
7
|
+
} from "./chunk-UOGTEMIJ.mjs";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_SCREENSHOT_STUDIO_CONFIG
|
|
10
|
+
} from "./chunk-Z6IB4BQV.mjs";
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_CANVAS_CONFIG
|
|
13
|
+
} from "./chunk-MC6PNV2F.mjs";
|
|
14
|
+
|
|
15
|
+
// src/presentation/hooks/useScreenshotStudio.ts
|
|
16
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
17
|
+
var DEFAULT_SCREENS = [
|
|
18
|
+
{
|
|
19
|
+
id: "1",
|
|
20
|
+
title: "APPGROWTHFACTORY *MODERN*",
|
|
21
|
+
subtitle: "Create content with AI power",
|
|
22
|
+
screenshot: null,
|
|
23
|
+
translations: {
|
|
24
|
+
"en-US": {
|
|
25
|
+
title: "APPGROWTHFACTORY *MODERN*",
|
|
26
|
+
subtitle: "Create content with AI power"
|
|
27
|
+
},
|
|
28
|
+
"tr-TR": {
|
|
29
|
+
title: "APPGROWTHFACTORY *MODERN*",
|
|
30
|
+
subtitle: "Yapay zeka g\xFCc\xFCyle i\xE7erik \xFCret"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
imageFit: "cover",
|
|
34
|
+
imageZoom: 1,
|
|
35
|
+
imageAnchorX: 50,
|
|
36
|
+
imageAnchorY: 50
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "2",
|
|
40
|
+
title: "CONNECT *GLOBAL*",
|
|
41
|
+
subtitle: "Post to all platforms instantly",
|
|
42
|
+
screenshot: null,
|
|
43
|
+
translations: {
|
|
44
|
+
"en-US": {
|
|
45
|
+
title: "CONNECT *GLOBAL*",
|
|
46
|
+
subtitle: "Post to all platforms instantly"
|
|
47
|
+
},
|
|
48
|
+
"tr-TR": {
|
|
49
|
+
title: "BA\u011ELAN *K\xDCRESEL*",
|
|
50
|
+
subtitle: "T\xFCm platformlara an\u0131nda g\xF6nder"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
imageFit: "cover",
|
|
54
|
+
imageZoom: 1,
|
|
55
|
+
imageAnchorX: 50,
|
|
56
|
+
imageAnchorY: 50
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
var DEFAULT_DESIGN = {
|
|
60
|
+
backgroundColor: "#6366f1",
|
|
61
|
+
backgroundGradientEnd: "#a855f7",
|
|
62
|
+
backgroundImage: null,
|
|
63
|
+
backgroundBlur: 0,
|
|
64
|
+
backgroundOverlayOpacity: 0,
|
|
65
|
+
backgroundPattern: "none",
|
|
66
|
+
textColor: "#ffffff",
|
|
67
|
+
highlightColor: "#fbbf24",
|
|
68
|
+
textAlign: "center",
|
|
69
|
+
deviceColor: "#1f2937",
|
|
70
|
+
deviceType: "iphone-15-pro",
|
|
71
|
+
frameStyle: "realistic",
|
|
72
|
+
layout: "text-top",
|
|
73
|
+
scale: 0.85,
|
|
74
|
+
fontFamily: "Inter",
|
|
75
|
+
titleFontSize: 60,
|
|
76
|
+
subtitleFontSize: 24,
|
|
77
|
+
titleFontWeight: "900",
|
|
78
|
+
subtitleFontWeight: "500",
|
|
79
|
+
textCase: "uppercase",
|
|
80
|
+
lineHeight: 0.9,
|
|
81
|
+
letterSpacing: -1,
|
|
82
|
+
rotation: 0,
|
|
83
|
+
rotationX: 0,
|
|
84
|
+
rotationY: 0,
|
|
85
|
+
rotationZ: 0,
|
|
86
|
+
offsetY: 0,
|
|
87
|
+
shadowIntensity: 0.5,
|
|
88
|
+
padding: 40,
|
|
89
|
+
gap: 20,
|
|
90
|
+
isPanoramic: false
|
|
91
|
+
};
|
|
92
|
+
var DEFAULT_APP_METADATA = {
|
|
93
|
+
appName: "Growdo",
|
|
94
|
+
subtitle: "AI Content Generator",
|
|
95
|
+
promotionalText: "Unlock your creative potential",
|
|
96
|
+
keywords: "ai, content, marketing, automation, generator",
|
|
97
|
+
description: "The ultimate tool for AI-powered marketing content.",
|
|
98
|
+
whatsNew: "New Design Studio integration!"
|
|
99
|
+
};
|
|
100
|
+
function useScreenshotStudio(options = {}) {
|
|
101
|
+
const {
|
|
102
|
+
config = {},
|
|
103
|
+
onShowToast = () => {
|
|
104
|
+
}
|
|
105
|
+
} = options;
|
|
106
|
+
const fullConfig = {
|
|
107
|
+
...DEFAULT_SCREENSHOT_STUDIO_CONFIG,
|
|
108
|
+
...config
|
|
109
|
+
};
|
|
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
|
+
}, []);
|
|
120
|
+
const [screens, setScreens] = useState(DEFAULT_SCREENS);
|
|
121
|
+
const [design, setDesign] = useState(DEFAULT_DESIGN);
|
|
122
|
+
const [currentLanguage, setCurrentLanguage] = useState(
|
|
123
|
+
fullConfig.defaultLanguage || "en-US"
|
|
124
|
+
);
|
|
125
|
+
const [selectedScreenId, setSelectedScreenId] = useState("1");
|
|
126
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
127
|
+
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
|
128
|
+
const [appMetadata, setAppMetadata] = useState(
|
|
129
|
+
DEFAULT_APP_METADATA
|
|
130
|
+
);
|
|
131
|
+
const [appIcon, setAppIcon] = useState(null);
|
|
132
|
+
const [projectId, setProjectId] = useState(null);
|
|
133
|
+
const activeScreen = screens.find((s) => s.id === selectedScreenId) || screens[0];
|
|
134
|
+
const updateDesign = useCallback((updates) => {
|
|
135
|
+
setDesign((prev) => ({ ...prev, ...updates }));
|
|
136
|
+
}, []);
|
|
137
|
+
const updateActiveScreen = useCallback(
|
|
138
|
+
(updates) => {
|
|
139
|
+
setScreens(
|
|
140
|
+
(prev) => prev.map((screen) => {
|
|
141
|
+
if (screen.id !== selectedScreenId) return screen;
|
|
142
|
+
const updatedScreen = { ...screen, ...updates };
|
|
143
|
+
if (updates.title || updates.subtitle) {
|
|
144
|
+
updatedScreen.translations = {
|
|
145
|
+
...updatedScreen.translations,
|
|
146
|
+
[currentLanguage]: {
|
|
147
|
+
title: updatedScreen.title,
|
|
148
|
+
subtitle: updatedScreen.subtitle
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return updatedScreen;
|
|
153
|
+
})
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
[selectedScreenId, currentLanguage]
|
|
157
|
+
);
|
|
158
|
+
const changeLanguage = useCallback(
|
|
159
|
+
(newLanguage) => {
|
|
160
|
+
setCurrentLanguage(newLanguage);
|
|
161
|
+
setScreens(
|
|
162
|
+
(prev) => prev.map((screen) => {
|
|
163
|
+
const translation = screen.translations[newLanguage] || screen.translations["en-US"] || { title: screen.title, subtitle: screen.subtitle };
|
|
164
|
+
return {
|
|
165
|
+
...screen,
|
|
166
|
+
title: translation.title,
|
|
167
|
+
subtitle: translation.subtitle
|
|
168
|
+
};
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
onShowToast("Language Changed", `Switched to ${newLanguage}`);
|
|
172
|
+
},
|
|
173
|
+
[onShowToast]
|
|
174
|
+
);
|
|
175
|
+
const addScreen = useCallback(() => {
|
|
176
|
+
const newScreen = {
|
|
177
|
+
id: Date.now().toString(),
|
|
178
|
+
title: "NEW *POST*",
|
|
179
|
+
subtitle: "Engage your audience",
|
|
180
|
+
screenshot: null,
|
|
181
|
+
translations: {
|
|
182
|
+
[currentLanguage]: {
|
|
183
|
+
title: "NEW *POST*",
|
|
184
|
+
subtitle: "Engage your audience"
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
imageFit: "cover",
|
|
188
|
+
imageZoom: 1,
|
|
189
|
+
imageAnchorX: 50,
|
|
190
|
+
imageAnchorY: 50
|
|
191
|
+
};
|
|
192
|
+
setScreens((prev) => [...prev, newScreen]);
|
|
193
|
+
setSelectedScreenId(newScreen.id);
|
|
194
|
+
onShowToast("Success", "New screen added");
|
|
195
|
+
}, [currentLanguage, onShowToast]);
|
|
196
|
+
const removeScreen = useCallback(
|
|
197
|
+
(screenId) => {
|
|
198
|
+
if (screens.length <= 1) return;
|
|
199
|
+
setScreens((prev) => {
|
|
200
|
+
const newScreens = prev.filter((s) => s.id !== screenId);
|
|
201
|
+
if (selectedScreenId === screenId) {
|
|
202
|
+
setSelectedScreenId(newScreens[0].id);
|
|
203
|
+
}
|
|
204
|
+
return newScreens;
|
|
205
|
+
});
|
|
206
|
+
onShowToast("Info", "Screen removed");
|
|
207
|
+
},
|
|
208
|
+
[screens.length, selectedScreenId, onShowToast]
|
|
209
|
+
);
|
|
210
|
+
const duplicateScreen = useCallback(
|
|
211
|
+
(screenId) => {
|
|
212
|
+
const screenToDuplicate = screens.find((s) => s.id === screenId);
|
|
213
|
+
if (!screenToDuplicate) return;
|
|
214
|
+
const newScreen = {
|
|
215
|
+
...screenToDuplicate,
|
|
216
|
+
id: Date.now().toString(),
|
|
217
|
+
title: `${screenToDuplicate.title} (Copy)`,
|
|
218
|
+
translations: { ...screenToDuplicate.translations }
|
|
219
|
+
};
|
|
220
|
+
setScreens((prev) => {
|
|
221
|
+
const index = prev.findIndex((s) => s.id === screenId);
|
|
222
|
+
const newScreens = [...prev];
|
|
223
|
+
newScreens.splice(index + 1, 0, newScreen);
|
|
224
|
+
return newScreens;
|
|
225
|
+
});
|
|
226
|
+
setSelectedScreenId(newScreen.id);
|
|
227
|
+
onShowToast("Success", "Screen duplicated");
|
|
228
|
+
},
|
|
229
|
+
[screens, onShowToast]
|
|
230
|
+
);
|
|
231
|
+
const applyTemplate = useCallback(
|
|
232
|
+
(template) => {
|
|
233
|
+
setDesign((prev) => ({ ...prev, ...template.design }));
|
|
234
|
+
onShowToast("Template Applied", template.name);
|
|
235
|
+
},
|
|
236
|
+
[onShowToast]
|
|
237
|
+
);
|
|
238
|
+
const generateDesignedScreenshot = useCallback(
|
|
239
|
+
async (screenId) => {
|
|
240
|
+
if (!serviceRef.current) return;
|
|
241
|
+
const screen = screens.find((s) => s.id === screenId);
|
|
242
|
+
if (!screen) return;
|
|
243
|
+
setIsProcessing(true);
|
|
244
|
+
try {
|
|
245
|
+
const result = await serviceRef.current.generateDesignedScreenshot(
|
|
246
|
+
screen,
|
|
247
|
+
design
|
|
248
|
+
);
|
|
249
|
+
onShowToast("Success", "Screenshot generated");
|
|
250
|
+
return { url: result.url, id: result.id };
|
|
251
|
+
} catch {
|
|
252
|
+
onShowToast("Error", "Screenshot generation failed");
|
|
253
|
+
} finally {
|
|
254
|
+
setIsProcessing(false);
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
[screens, design, onShowToast]
|
|
258
|
+
);
|
|
259
|
+
const exportAllScreensAsZip = useCallback(
|
|
260
|
+
async (format = "png") => {
|
|
261
|
+
if (!serviceRef.current) return;
|
|
262
|
+
setIsProcessing(true);
|
|
263
|
+
let url = null;
|
|
264
|
+
try {
|
|
265
|
+
const project = {
|
|
266
|
+
id: projectId || "default-project",
|
|
267
|
+
name: "Export Project",
|
|
268
|
+
design,
|
|
269
|
+
screens,
|
|
270
|
+
currentLanguage,
|
|
271
|
+
selectedScreenId,
|
|
272
|
+
appMetadata,
|
|
273
|
+
appIcon,
|
|
274
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
275
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
276
|
+
};
|
|
277
|
+
const blob = await serviceRef.current.exportProjectAsZip(project);
|
|
278
|
+
url = URL.createObjectURL(blob);
|
|
279
|
+
const link = document.createElement("a");
|
|
280
|
+
link.href = url;
|
|
281
|
+
link.download = `Studio_${currentLanguage}_${Date.now()}.zip`;
|
|
282
|
+
link.click();
|
|
283
|
+
onShowToast(
|
|
284
|
+
"Success",
|
|
285
|
+
`Exported ${screens.length} screens in ZIP archive.`
|
|
286
|
+
);
|
|
287
|
+
} catch {
|
|
288
|
+
onShowToast("Error", "ZIP export failed");
|
|
289
|
+
} finally {
|
|
290
|
+
setIsProcessing(false);
|
|
291
|
+
if (url) {
|
|
292
|
+
setTimeout(() => {
|
|
293
|
+
try {
|
|
294
|
+
URL.revokeObjectURL(url);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
}
|
|
297
|
+
}, 100);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
[
|
|
302
|
+
screens,
|
|
303
|
+
design,
|
|
304
|
+
currentLanguage,
|
|
305
|
+
appMetadata,
|
|
306
|
+
appIcon,
|
|
307
|
+
projectId,
|
|
308
|
+
selectedScreenId,
|
|
309
|
+
onShowToast
|
|
310
|
+
]
|
|
311
|
+
);
|
|
312
|
+
const _getProjectState = useCallback(() => {
|
|
313
|
+
return {
|
|
314
|
+
id: projectId || "appgrowthfactory_main_project",
|
|
315
|
+
name: "Default Project",
|
|
316
|
+
design,
|
|
317
|
+
screens,
|
|
318
|
+
currentLanguage,
|
|
319
|
+
selectedScreenId,
|
|
320
|
+
appMetadata,
|
|
321
|
+
appIcon,
|
|
322
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
323
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
324
|
+
};
|
|
325
|
+
}, [
|
|
326
|
+
design,
|
|
327
|
+
screens,
|
|
328
|
+
currentLanguage,
|
|
329
|
+
selectedScreenId,
|
|
330
|
+
appMetadata,
|
|
331
|
+
appIcon,
|
|
332
|
+
projectId
|
|
333
|
+
]);
|
|
334
|
+
const _loadProjectState = useCallback(
|
|
335
|
+
(state) => {
|
|
336
|
+
if (!state) return;
|
|
337
|
+
if (state.screens) setScreens(state.screens);
|
|
338
|
+
if (state.design) setDesign(state.design);
|
|
339
|
+
if (state.currentLanguage) setCurrentLanguage(state.currentLanguage);
|
|
340
|
+
if (state.selectedScreenId && state.screens?.find((s) => s.id === state.selectedScreenId)) {
|
|
341
|
+
setSelectedScreenId(state.selectedScreenId);
|
|
342
|
+
}
|
|
343
|
+
if (state.appMetadata) setAppMetadata(state.appMetadata);
|
|
344
|
+
if (state.appIcon !== void 0) setAppIcon(state.appIcon);
|
|
345
|
+
},
|
|
346
|
+
[]
|
|
347
|
+
);
|
|
348
|
+
return {
|
|
349
|
+
screens,
|
|
350
|
+
design,
|
|
351
|
+
currentLanguage,
|
|
352
|
+
selectedScreenId,
|
|
353
|
+
activeScreen,
|
|
354
|
+
isProcessing,
|
|
355
|
+
isAnalyzing,
|
|
356
|
+
appMetadata,
|
|
357
|
+
appIcon,
|
|
358
|
+
projectId,
|
|
359
|
+
updateDesign,
|
|
360
|
+
updateActiveScreen,
|
|
361
|
+
setSelectedScreenId,
|
|
362
|
+
changeLanguage,
|
|
363
|
+
applyTemplate,
|
|
364
|
+
addScreen,
|
|
365
|
+
removeScreen,
|
|
366
|
+
duplicateScreen,
|
|
367
|
+
generateDesignedScreenshot,
|
|
368
|
+
exportAllScreensAsZip,
|
|
369
|
+
setAppMetadata,
|
|
370
|
+
setAppIcon,
|
|
371
|
+
setProjectId,
|
|
372
|
+
_getProjectState,
|
|
373
|
+
_loadProjectState
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/presentation/hooks/useCanvasExport.ts
|
|
378
|
+
import { useState as useState2, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect2 } from "react";
|
|
379
|
+
function useCanvasExport(options = {}) {
|
|
380
|
+
const { config = DEFAULT_CANVAS_CONFIG } = options;
|
|
381
|
+
const [isExporting, setIsExporting] = useState2(false);
|
|
382
|
+
const [progress, setProgress] = useState2(0);
|
|
383
|
+
const [error, setError] = useState2(null);
|
|
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
|
+
}, []);
|
|
400
|
+
const exportToBlob = useCallback2(
|
|
401
|
+
async (canvas, options2) => {
|
|
402
|
+
if (!rendererRef.current) return null;
|
|
403
|
+
setIsExporting(true);
|
|
404
|
+
setProgress(0);
|
|
405
|
+
setError(null);
|
|
406
|
+
try {
|
|
407
|
+
const result = await rendererRef.current.exportToBlob(canvas, options2);
|
|
408
|
+
setProgress(100);
|
|
409
|
+
if (result?.url) {
|
|
410
|
+
urlCleanupRef.current.push(result.url);
|
|
411
|
+
}
|
|
412
|
+
return result;
|
|
413
|
+
} catch (err) {
|
|
414
|
+
const errorMessage = err instanceof Error ? err.message : "Export failed";
|
|
415
|
+
setError(errorMessage);
|
|
416
|
+
return null;
|
|
417
|
+
} finally {
|
|
418
|
+
setIsExporting(false);
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
[]
|
|
422
|
+
);
|
|
423
|
+
const exportToDataURL = useCallback2(
|
|
424
|
+
(canvas, format, quality) => {
|
|
425
|
+
if (!rendererRef.current) return null;
|
|
426
|
+
try {
|
|
427
|
+
return rendererRef.current.exportToDataURL(canvas, format, quality);
|
|
428
|
+
} catch (err) {
|
|
429
|
+
const errorMessage = err instanceof Error ? err.message : "Export failed";
|
|
430
|
+
setError(errorMessage);
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
[]
|
|
435
|
+
);
|
|
436
|
+
const downloadCanvas = useCallback2(
|
|
437
|
+
async (canvas, filename, options2) => {
|
|
438
|
+
setIsExporting(true);
|
|
439
|
+
setProgress(0);
|
|
440
|
+
setError(null);
|
|
441
|
+
try {
|
|
442
|
+
const result = await exportToBlob(canvas, options2);
|
|
443
|
+
if (!result) {
|
|
444
|
+
throw new Error("Export failed");
|
|
445
|
+
}
|
|
446
|
+
setProgress(50);
|
|
447
|
+
const link = document.createElement("a");
|
|
448
|
+
link.href = result.url;
|
|
449
|
+
link.download = filename;
|
|
450
|
+
link.click();
|
|
451
|
+
setProgress(100);
|
|
452
|
+
setTimeout(() => {
|
|
453
|
+
URL.revokeObjectURL(result.url);
|
|
454
|
+
const index = urlCleanupRef.current.indexOf(result.url);
|
|
455
|
+
if (index > -1) {
|
|
456
|
+
urlCleanupRef.current.splice(index, 1);
|
|
457
|
+
}
|
|
458
|
+
}, 100);
|
|
459
|
+
} catch (err) {
|
|
460
|
+
const errorMessage = err instanceof Error ? err.message : "Download failed";
|
|
461
|
+
setError(errorMessage);
|
|
462
|
+
} finally {
|
|
463
|
+
setIsExporting(false);
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
[exportToBlob]
|
|
467
|
+
);
|
|
468
|
+
const cloneCanvas = useCallback2(
|
|
469
|
+
(canvas) => {
|
|
470
|
+
if (!rendererRef.current) return null;
|
|
471
|
+
try {
|
|
472
|
+
return rendererRef.current.cloneCanvas(canvas);
|
|
473
|
+
} catch (err) {
|
|
474
|
+
const errorMessage = err instanceof Error ? err.message : "Clone failed";
|
|
475
|
+
setError(errorMessage);
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
[]
|
|
480
|
+
);
|
|
481
|
+
return {
|
|
482
|
+
isExporting,
|
|
483
|
+
progress,
|
|
484
|
+
error,
|
|
485
|
+
exportToBlob,
|
|
486
|
+
exportToDataURL,
|
|
487
|
+
downloadCanvas,
|
|
488
|
+
cloneCanvas
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/presentation/hooks/useReportExport.ts
|
|
493
|
+
import { useState as useState3, useCallback as useCallback3, useRef as useRef3 } from "react";
|
|
494
|
+
function useReportExport() {
|
|
495
|
+
const [isExporting, setIsExporting] = useState3(false);
|
|
496
|
+
const [error, setError] = useState3(null);
|
|
497
|
+
const reportServiceRef = useRef3(null);
|
|
498
|
+
if (!reportServiceRef.current) {
|
|
499
|
+
reportServiceRef.current = ReportService.getInstance();
|
|
500
|
+
}
|
|
501
|
+
const exportToPDF = useCallback3(
|
|
502
|
+
async (elementId, options) => {
|
|
503
|
+
if (!reportServiceRef.current) {
|
|
504
|
+
throw new Error("ReportService not initialized");
|
|
505
|
+
}
|
|
506
|
+
setIsExporting(true);
|
|
507
|
+
setError(null);
|
|
508
|
+
try {
|
|
509
|
+
await reportServiceRef.current.exportToPDF(elementId, options);
|
|
510
|
+
} catch (err) {
|
|
511
|
+
const errorMessage = err instanceof Error ? err.message : "PDF export failed";
|
|
512
|
+
setError(errorMessage);
|
|
513
|
+
throw err;
|
|
514
|
+
} finally {
|
|
515
|
+
setIsExporting(false);
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
[]
|
|
519
|
+
);
|
|
520
|
+
const exportCanvasToPDF = useCallback3(
|
|
521
|
+
async (canvas, options) => {
|
|
522
|
+
if (!reportServiceRef.current) {
|
|
523
|
+
throw new Error("ReportService not initialized");
|
|
524
|
+
}
|
|
525
|
+
setIsExporting(true);
|
|
526
|
+
setError(null);
|
|
527
|
+
try {
|
|
528
|
+
await reportServiceRef.current.exportCanvasToPDF(canvas, options);
|
|
529
|
+
} catch (err) {
|
|
530
|
+
const errorMessage = err instanceof Error ? err.message : "Canvas PDF export failed";
|
|
531
|
+
setError(errorMessage);
|
|
532
|
+
throw err;
|
|
533
|
+
} finally {
|
|
534
|
+
setIsExporting(false);
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
[]
|
|
538
|
+
);
|
|
539
|
+
const exportMultipleToPDF = useCallback3(
|
|
540
|
+
async (elementIds, options) => {
|
|
541
|
+
if (!reportServiceRef.current) {
|
|
542
|
+
throw new Error("ReportService not initialized");
|
|
543
|
+
}
|
|
544
|
+
setIsExporting(true);
|
|
545
|
+
setError(null);
|
|
546
|
+
try {
|
|
547
|
+
await reportServiceRef.current.exportMultipleToPDF(elementIds, options);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
const errorMessage = err instanceof Error ? err.message : "Multi-page PDF export failed";
|
|
550
|
+
setError(errorMessage);
|
|
551
|
+
throw err;
|
|
552
|
+
} finally {
|
|
553
|
+
setIsExporting(false);
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
[]
|
|
557
|
+
);
|
|
558
|
+
const exportToExcel = useCallback3(
|
|
559
|
+
async (data, options) => {
|
|
560
|
+
if (!reportServiceRef.current) {
|
|
561
|
+
throw new Error("ReportService not initialized");
|
|
562
|
+
}
|
|
563
|
+
setIsExporting(true);
|
|
564
|
+
setError(null);
|
|
565
|
+
try {
|
|
566
|
+
await reportServiceRef.current.exportToExcel(data, options);
|
|
567
|
+
} catch (err) {
|
|
568
|
+
const errorMessage = err instanceof Error ? err.message : "Excel export failed";
|
|
569
|
+
setError(errorMessage);
|
|
570
|
+
throw err;
|
|
571
|
+
} finally {
|
|
572
|
+
setIsExporting(false);
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
[]
|
|
576
|
+
);
|
|
577
|
+
const exportToCSV = useCallback3(
|
|
578
|
+
(data, filename, delimiter) => {
|
|
579
|
+
if (!reportServiceRef.current) {
|
|
580
|
+
throw new Error("ReportService not initialized");
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
reportServiceRef.current.exportToCSV(data, filename, delimiter);
|
|
584
|
+
} catch (err) {
|
|
585
|
+
const errorMessage = err instanceof Error ? err.message : "CSV export failed";
|
|
586
|
+
setError(errorMessage);
|
|
587
|
+
throw err;
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
[]
|
|
591
|
+
);
|
|
592
|
+
return {
|
|
593
|
+
isExporting,
|
|
594
|
+
error,
|
|
595
|
+
exportToPDF,
|
|
596
|
+
exportCanvasToPDF,
|
|
597
|
+
exportMultipleToPDF,
|
|
598
|
+
exportToExcel,
|
|
599
|
+
exportToCSV
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/presentation/hooks/useFreehandCanvas.ts
|
|
604
|
+
import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
|
|
605
|
+
var DEFAULT_BRUSH_SIZE = 3;
|
|
606
|
+
var DEFAULT_COLOR = "#1a1a1a";
|
|
607
|
+
function widthFor(stroke, point) {
|
|
608
|
+
if (!stroke.dynamic) return stroke.size;
|
|
609
|
+
return stroke.size * (0.5 + Math.min(1, Math.max(0, point.pressure)));
|
|
610
|
+
}
|
|
611
|
+
function useFreehandCanvas(options = {}) {
|
|
612
|
+
const { brushSize = DEFAULT_BRUSH_SIZE, color = DEFAULT_COLOR, pressureEnabled = true } = options;
|
|
613
|
+
const optionsRef = useRef4({ brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd });
|
|
614
|
+
optionsRef.current = { brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd };
|
|
615
|
+
const canvasRef = useRef4(null);
|
|
616
|
+
const strokesRef = useRef4([]);
|
|
617
|
+
const activeStrokeRef = useRef4(null);
|
|
618
|
+
const dprRef = useRef4(1);
|
|
619
|
+
const [strokeCount, setStrokeCount] = useState4(0);
|
|
620
|
+
const redraw = useCallback4(() => {
|
|
621
|
+
const canvas = canvasRef.current;
|
|
622
|
+
const ctx = canvas?.getContext("2d");
|
|
623
|
+
if (!canvas || !ctx) return;
|
|
624
|
+
const dpr = dprRef.current;
|
|
625
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
626
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
627
|
+
ctx.scale(dpr, dpr);
|
|
628
|
+
const all = activeStrokeRef.current ? [...strokesRef.current, activeStrokeRef.current] : strokesRef.current;
|
|
629
|
+
ctx.lineCap = "round";
|
|
630
|
+
ctx.lineJoin = "round";
|
|
631
|
+
for (const stroke of all) {
|
|
632
|
+
ctx.strokeStyle = stroke.color;
|
|
633
|
+
ctx.fillStyle = stroke.color;
|
|
634
|
+
if (stroke.points.length === 1) {
|
|
635
|
+
const point = stroke.points[0];
|
|
636
|
+
ctx.beginPath();
|
|
637
|
+
ctx.arc(point.x, point.y, widthFor(stroke, point) / 2, 0, Math.PI * 2);
|
|
638
|
+
ctx.fill();
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
for (let i = 1; i < stroke.points.length; i += 1) {
|
|
642
|
+
const from = stroke.points[i - 1];
|
|
643
|
+
const to = stroke.points[i];
|
|
644
|
+
ctx.beginPath();
|
|
645
|
+
ctx.lineWidth = (widthFor(stroke, from) + widthFor(stroke, to)) / 2;
|
|
646
|
+
ctx.moveTo(from.x, from.y);
|
|
647
|
+
ctx.lineTo(to.x, to.y);
|
|
648
|
+
ctx.stroke();
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}, []);
|
|
652
|
+
const syncDimensions = useCallback4(() => {
|
|
653
|
+
const canvas = canvasRef.current;
|
|
654
|
+
if (!canvas) return false;
|
|
655
|
+
const dpr = window.devicePixelRatio || 1;
|
|
656
|
+
const width = Math.max(1, Math.round(canvas.clientWidth * dpr));
|
|
657
|
+
const height = Math.max(1, Math.round(canvas.clientHeight * dpr));
|
|
658
|
+
if (canvas.width === width && canvas.height === height && dprRef.current === dpr) return false;
|
|
659
|
+
canvas.width = width;
|
|
660
|
+
canvas.height = height;
|
|
661
|
+
dprRef.current = dpr;
|
|
662
|
+
return true;
|
|
663
|
+
}, []);
|
|
664
|
+
useEffect4(() => {
|
|
665
|
+
const canvas = canvasRef.current;
|
|
666
|
+
if (!canvas) return;
|
|
667
|
+
const el = canvas;
|
|
668
|
+
el.style.touchAction = "none";
|
|
669
|
+
syncDimensions();
|
|
670
|
+
redraw();
|
|
671
|
+
const observer = new ResizeObserver(() => {
|
|
672
|
+
if (syncDimensions()) redraw();
|
|
673
|
+
});
|
|
674
|
+
observer.observe(canvas);
|
|
675
|
+
function pointFrom(event) {
|
|
676
|
+
const rect = el.getBoundingClientRect();
|
|
677
|
+
return {
|
|
678
|
+
x: event.clientX - rect.left,
|
|
679
|
+
y: event.clientY - rect.top,
|
|
680
|
+
pressure: event.pressure > 0 ? event.pressure : 0.5
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function drawSegment(stroke, from, to) {
|
|
684
|
+
const ctx = el.getContext("2d");
|
|
685
|
+
if (!ctx) return;
|
|
686
|
+
ctx.lineCap = "round";
|
|
687
|
+
ctx.lineJoin = "round";
|
|
688
|
+
ctx.strokeStyle = stroke.color;
|
|
689
|
+
ctx.lineWidth = (widthFor(stroke, from) + widthFor(stroke, to)) / 2;
|
|
690
|
+
ctx.beginPath();
|
|
691
|
+
ctx.moveTo(from.x, from.y);
|
|
692
|
+
ctx.lineTo(to.x, to.y);
|
|
693
|
+
ctx.stroke();
|
|
694
|
+
}
|
|
695
|
+
function drawDot(stroke, point) {
|
|
696
|
+
const ctx = el.getContext("2d");
|
|
697
|
+
if (!ctx) return;
|
|
698
|
+
ctx.fillStyle = stroke.color;
|
|
699
|
+
ctx.beginPath();
|
|
700
|
+
ctx.arc(point.x, point.y, widthFor(stroke, point) / 2, 0, Math.PI * 2);
|
|
701
|
+
ctx.fill();
|
|
702
|
+
}
|
|
703
|
+
function onPointerDown(event) {
|
|
704
|
+
if (event.button !== 0 && event.pointerType === "mouse") return;
|
|
705
|
+
event.preventDefault();
|
|
706
|
+
el.setPointerCapture(event.pointerId);
|
|
707
|
+
const { brushSize: size, color: ink, pressureEnabled: dynamic } = optionsRef.current;
|
|
708
|
+
const stroke = {
|
|
709
|
+
size,
|
|
710
|
+
color: ink,
|
|
711
|
+
dynamic: dynamic && event.pointerType === "pen",
|
|
712
|
+
points: [pointFrom(event)]
|
|
713
|
+
};
|
|
714
|
+
activeStrokeRef.current = stroke;
|
|
715
|
+
drawDot(stroke, stroke.points[0]);
|
|
716
|
+
}
|
|
717
|
+
function onPointerMove(event) {
|
|
718
|
+
const stroke = activeStrokeRef.current;
|
|
719
|
+
if (!stroke) return;
|
|
720
|
+
event.preventDefault();
|
|
721
|
+
const point = pointFrom(event);
|
|
722
|
+
const previous = stroke.points[stroke.points.length - 1];
|
|
723
|
+
stroke.points.push(point);
|
|
724
|
+
drawSegment(stroke, previous, point);
|
|
725
|
+
}
|
|
726
|
+
function commit(event) {
|
|
727
|
+
const stroke = activeStrokeRef.current;
|
|
728
|
+
if (!stroke) return;
|
|
729
|
+
activeStrokeRef.current = null;
|
|
730
|
+
if (el.hasPointerCapture(event.pointerId)) {
|
|
731
|
+
el.releasePointerCapture(event.pointerId);
|
|
732
|
+
}
|
|
733
|
+
strokesRef.current = [...strokesRef.current, stroke];
|
|
734
|
+
setStrokeCount(strokesRef.current.length);
|
|
735
|
+
optionsRef.current.onStrokeEnd?.(stroke);
|
|
736
|
+
}
|
|
737
|
+
el.addEventListener("pointerdown", onPointerDown);
|
|
738
|
+
el.addEventListener("pointermove", onPointerMove);
|
|
739
|
+
el.addEventListener("pointerup", commit);
|
|
740
|
+
el.addEventListener("pointercancel", commit);
|
|
741
|
+
return () => {
|
|
742
|
+
observer.disconnect();
|
|
743
|
+
el.removeEventListener("pointerdown", onPointerDown);
|
|
744
|
+
el.removeEventListener("pointermove", onPointerMove);
|
|
745
|
+
el.removeEventListener("pointerup", commit);
|
|
746
|
+
el.removeEventListener("pointercancel", commit);
|
|
747
|
+
};
|
|
748
|
+
}, [redraw, syncDimensions]);
|
|
749
|
+
const undo = useCallback4(() => {
|
|
750
|
+
if (strokesRef.current.length === 0) return;
|
|
751
|
+
strokesRef.current = strokesRef.current.slice(0, -1);
|
|
752
|
+
setStrokeCount(strokesRef.current.length);
|
|
753
|
+
redraw();
|
|
754
|
+
}, [redraw]);
|
|
755
|
+
const clear = useCallback4(() => {
|
|
756
|
+
if (strokesRef.current.length === 0) return;
|
|
757
|
+
strokesRef.current = [];
|
|
758
|
+
setStrokeCount(0);
|
|
759
|
+
redraw();
|
|
760
|
+
}, [redraw]);
|
|
761
|
+
const getStrokes = useCallback4(() => strokesRef.current.map((stroke) => ({ ...stroke, points: stroke.points.map((point) => ({ ...point })) })), []);
|
|
762
|
+
return {
|
|
763
|
+
canvasRef,
|
|
764
|
+
undo,
|
|
765
|
+
clear,
|
|
766
|
+
strokeCount,
|
|
767
|
+
isEmpty: strokeCount === 0,
|
|
768
|
+
getStrokes
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export {
|
|
773
|
+
useScreenshotStudio,
|
|
774
|
+
useCanvasExport,
|
|
775
|
+
useReportExport,
|
|
776
|
+
useFreehandCanvas
|
|
777
|
+
};
|