PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / 0.0.4
ZIP AI – AI Website Builder & AI Agent (Beta) v0.0.4
0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / src / store / useScreenshotStore.js

useScreenshotStore.js in ZIP AI – AI Website Builder & AI Agent (Beta) 0.0.4, at src/store/useScreenshotStore.js

266 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { create } from 'zustand';
2 // html2canvas-pro fork: drop-in replacement for html2canvas with modern-CSS
3 // support (oklch, lab, lch, color() function). Stock html2canvas@1.4.1 fails
4 // on WP themes using CSS Color Module Level 4 syntax.
5 import html2canvas from 'html2canvas-pro';
6
7 // Annotation state lives outside Zustand — canvas refs must not trigger renders
8 let annotationOverlay = null;
9 const annotationStrokes = [];
10 let annotationDrawing = false;
11
12 const captureVisibleViewport = async () => {
13 const editorIframe = document.querySelector('iframe[name="editor-canvas"]');
14 let target, opts;
15
16 if (editorIframe?.contentDocument?.body && editorIframe?.contentWindow) {
17 const iDoc = editorIframe.contentDocument;
18 const scrollTop = iDoc.documentElement.scrollTop || 0;
19 const scrollLeft = iDoc.documentElement.scrollLeft || 0;
20 target = iDoc.documentElement;
21 opts = {
22 useCORS: true, allowTaint: true, scale: 1, logging: false,
23 x: scrollLeft, y: scrollTop,
24 width: editorIframe.clientWidth, height: editorIframe.clientHeight,
25 windowWidth: editorIframe.clientWidth, windowHeight: editorIframe.clientHeight,
26 };
27 } else {
28 target = document.querySelector('#wpwrap') || document.body;
29 opts = {
30 useCORS: true, allowTaint: true, scale: 1, logging: false,
31 x: window.scrollX, y: window.scrollY,
32 width: window.innerWidth, height: window.innerHeight,
33 windowWidth: window.innerWidth, windowHeight: window.innerHeight,
34 };
35 }
36
37 const canvas = await html2canvas(target, opts);
38 let finalCanvas = canvas;
39 const maxWidth = 1280;
40 if (canvas.width > maxWidth) {
41 finalCanvas = document.createElement('canvas');
42 const ratio = maxWidth / canvas.width;
43 finalCanvas.width = maxWidth;
44 finalCanvas.height = Math.round(canvas.height * ratio);
45 finalCanvas.getContext('2d').drawImage(canvas, 0, 0, finalCanvas.width, finalCanvas.height);
46 }
47 return finalCanvas.toDataURL('image/jpeg', 0.75);
48 };
49
50 const removeAnnotationOverlay = () => {
51 const existing =
52 annotationOverlay ||
53 document.getElementById('zip-ai-annotation-overlay') ||
54 document.querySelector('iframe[name="editor-canvas"]')?.contentDocument?.getElementById('zip-ai-annotation-overlay');
55 if (existing) existing.remove();
56 annotationOverlay = null;
57 annotationStrokes.length = 0;
58 };
59
60 export const useScreenshotStore = create((set, get) => ({
61 showScreenshotPanel: false,
62 isCapturing: false,
63 isAnnotating: false,
64 capturedImage: null,
65
66 openScreenshotPanel: () => set({ showScreenshotPanel: true }),
67
68 closeScreenshotPanel() {
69 set({ showScreenshotPanel: false, isCapturing: false, isAnnotating: false });
70 },
71
72 async captureScreenshot() {
73 set({ isCapturing: true });
74 try {
75 const dataUrl = await captureVisibleViewport();
76 set({ capturedImage: { dataUrl, isAnnotated: false }, showScreenshotPanel: false });
77 } catch (err) {
78 } finally {
79 set({ isCapturing: false });
80 }
81 },
82
83 startAnnotation() {
84 // Remove any existing overlay
85 removeAnnotationOverlay();
86 annotationStrokes.length = 0;
87
88 const startOverlay = () => {
89 const editorIframe = document.querySelector('iframe[name="editor-canvas"]');
90 const overlay = document.createElement('canvas');
91 overlay.id = 'zip-ai-annotation-overlay';
92
93 const sidebarWidth =
94 getComputedStyle(document.documentElement).getPropertyValue('--zip-ai-sidebar-width')?.trim() || '420px';
95 const sidebarOpen = document.body.classList.contains('zip-ai-assistant-open');
96
97 if (editorIframe) {
98 const rect = editorIframe.getBoundingClientRect();
99 overlay.width = Math.round(rect.width);
100 overlay.height = Math.round(rect.height);
101 Object.assign(overlay.style, {
102 position: 'fixed', top: rect.top + 'px', left: rect.left + 'px',
103 width: rect.width + 'px', height: rect.height + 'px',
104 zIndex: '99997', cursor: 'crosshair', touchAction: 'none', pointerEvents: 'auto',
105 });
106 } else {
107 const rightOffset = sidebarOpen ? parseInt(sidebarWidth) : 0;
108 overlay.width = window.innerWidth - rightOffset;
109 overlay.height = window.innerHeight;
110 Object.assign(overlay.style, {
111 position: 'fixed', top: '0', left: '0',
112 right: sidebarOpen ? sidebarWidth : '0px',
113 height: '100vh', zIndex: '99997', cursor: 'crosshair', touchAction: 'none',
114 });
115 }
116
117 document.body.appendChild(overlay);
118 annotationOverlay = overlay;
119
120 const ctx = overlay.getContext('2d');
121 const STROKE_COLOR = '#ef4444';
122 const STROKE_WIDTH = 3;
123 let currentPoints = [];
124
125 const getPos = (e) => {
126 const rect = overlay.getBoundingClientRect();
127 const clientX = e.touches ? e.touches[0].clientX : e.clientX;
128 const clientY = e.touches ? e.touches[0].clientY : e.clientY;
129 return { x: clientX - rect.left, y: clientY - rect.top };
130 };
131
132 const redraw = () => {
133 ctx.clearRect(0, 0, overlay.width, overlay.height);
134 const all = [...annotationStrokes];
135 if (currentPoints.length > 1) all.push(currentPoints);
136 for (const stroke of all) {
137 if (stroke.length < 2) continue;
138 ctx.strokeStyle = STROKE_COLOR;
139 ctx.lineWidth = STROKE_WIDTH;
140 ctx.lineCap = 'round'; ctx.lineJoin = 'round';
141 ctx.beginPath();
142 ctx.moveTo(stroke[0].x, stroke[0].y);
143 for (let i = 1; i < stroke.length; i++) ctx.lineTo(stroke[i].x, stroke[i].y);
144 ctx.stroke();
145 }
146 };
147
148 overlay.addEventListener('pointerdown', (e) => {
149 e.preventDefault();
150 annotationDrawing = true;
151 currentPoints = [getPos(e)];
152 overlay.setPointerCapture(e.pointerId);
153 });
154 overlay.addEventListener('pointermove', (e) => {
155 if (!annotationDrawing) return;
156 e.preventDefault();
157 currentPoints.push(getPos(e));
158 redraw();
159 });
160 overlay.addEventListener('pointerup', (e) => {
161 if (!annotationDrawing) return;
162 e.preventDefault();
163 annotationDrawing = false;
164 if (currentPoints.length > 1) annotationStrokes.push([...currentPoints]);
165 currentPoints = [];
166 redraw();
167 });
168
169 set({ isAnnotating: true });
170 };
171
172 startOverlay();
173 },
174
175 annotationUndo() {
176 if (annotationStrokes.length > 0) {
177 annotationStrokes.pop();
178 const overlay = annotationOverlay;
179 if (overlay) {
180 const ctx = overlay.getContext('2d');
181 ctx.clearRect(0, 0, overlay.width, overlay.height);
182 for (const stroke of annotationStrokes) {
183 if (stroke.length < 2) continue;
184 ctx.strokeStyle = '#ef4444'; ctx.lineWidth = 3;
185 ctx.lineCap = 'round'; ctx.lineJoin = 'round';
186 ctx.beginPath();
187 ctx.moveTo(stroke[0].x, stroke[0].y);
188 for (let i = 1; i < stroke.length; i++) ctx.lineTo(stroke[i].x, stroke[i].y);
189 ctx.stroke();
190 }
191 }
192 }
193 },
194
195 annotationClear() {
196 annotationStrokes.length = 0;
197 if (annotationOverlay) {
198 annotationOverlay.getContext('2d').clearRect(0, 0, annotationOverlay.width, annotationOverlay.height);
199 }
200 },
201
202 annotationCancel() {
203 removeAnnotationOverlay();
204 set({ isAnnotating: false, showScreenshotPanel: false });
205 },
206
207 async annotationDone() {
208 set({ isCapturing: true });
209 try {
210 const strokes = [...annotationStrokes];
211 const overlay = annotationOverlay;
212 const overlayWidth = overlay?.width || window.innerWidth;
213 const overlayHeight = overlay?.height || window.innerHeight;
214
215 removeAnnotationOverlay();
216
217 const dataUrl = await captureVisibleViewport();
218 const img = new Image();
219 await new Promise((resolve, reject) => {
220 img.onload = resolve; img.onerror = reject; img.src = dataUrl;
221 });
222
223 const finalCanvas = document.createElement('canvas');
224 finalCanvas.width = img.width;
225 finalCanvas.height = img.height;
226 const ctx = finalCanvas.getContext('2d');
227 ctx.drawImage(img, 0, 0);
228
229 if (strokes.length > 0) {
230 const scaleX = finalCanvas.width / overlayWidth;
231 const scaleY = finalCanvas.height / overlayHeight;
232 for (const stroke of strokes) {
233 if (stroke.length < 2) continue;
234 ctx.strokeStyle = '#ef4444';
235 ctx.lineWidth = 3 * Math.max(scaleX, scaleY);
236 ctx.lineCap = 'round'; ctx.lineJoin = 'round';
237 ctx.beginPath();
238 ctx.moveTo(stroke[0].x * scaleX, stroke[0].y * scaleY);
239 for (let i = 1; i < stroke.length; i++) {
240 ctx.lineTo(stroke[i].x * scaleX, stroke[i].y * scaleY);
241 }
242 ctx.stroke();
243 }
244 }
245
246 const annotatedDataUrl = finalCanvas.toDataURL('image/jpeg', 0.75);
247 set({ capturedImage: { dataUrl: annotatedDataUrl, isAnnotated: true }, isAnnotating: false, showScreenshotPanel: false });
248 } catch (err) {
249 } finally {
250 set({ isCapturing: false });
251 }
252 },
253
254 setCapturedImageData(dataUrl, isAnnotated = false) {
255 set({ capturedImage: { dataUrl, isAnnotated }, isAnnotating: false, showScreenshotPanel: false });
256 },
257
258 clearCapturedImage: () => set({ capturedImage: null }),
259
260 /** Full reset — clears captured image, annotation overlay, and all state. */
261 resetAll() {
262 removeAnnotationOverlay();
263 set({ showScreenshotPanel: false, isCapturing: false, isAnnotating: false, capturedImage: null });
264 },
265 }));
266