@bugspotter/sdk 0.1.0-alpha.1
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/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +639 -0
- package/dist/bugspotter.min.js +2 -0
- package/dist/bugspotter.min.js.LICENSE.txt +14 -0
- package/dist/capture/base-capture.d.ts +34 -0
- package/dist/capture/base-capture.js +23 -0
- package/dist/capture/capture-lifecycle.d.ts +24 -0
- package/dist/capture/capture-lifecycle.js +2 -0
- package/dist/capture/console.d.ts +29 -0
- package/dist/capture/console.js +107 -0
- package/dist/capture/metadata.d.ts +21 -0
- package/dist/capture/metadata.js +76 -0
- package/dist/capture/network.d.ts +32 -0
- package/dist/capture/network.js +135 -0
- package/dist/capture/screenshot.d.ts +19 -0
- package/dist/capture/screenshot.js +52 -0
- package/dist/collectors/dom.d.ts +67 -0
- package/dist/collectors/dom.js +164 -0
- package/dist/collectors/index.d.ts +2 -0
- package/dist/collectors/index.js +5 -0
- package/dist/core/buffer.d.ts +50 -0
- package/dist/core/buffer.js +88 -0
- package/dist/core/circular-buffer.d.ts +42 -0
- package/dist/core/circular-buffer.js +77 -0
- package/dist/core/compress.d.ts +49 -0
- package/dist/core/compress.js +245 -0
- package/dist/core/offline-queue.d.ts +76 -0
- package/dist/core/offline-queue.js +301 -0
- package/dist/core/transport.d.ts +73 -0
- package/dist/core/transport.js +352 -0
- package/dist/core/upload-helpers.d.ts +32 -0
- package/dist/core/upload-helpers.js +79 -0
- package/dist/core/uploader.d.ts +70 -0
- package/dist/core/uploader.js +185 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.esm.js +205 -0
- package/dist/index.js +244 -0
- package/dist/utils/logger.d.ts +28 -0
- package/dist/utils/logger.js +84 -0
- package/dist/utils/sanitize-patterns.d.ts +103 -0
- package/dist/utils/sanitize-patterns.js +282 -0
- package/dist/utils/sanitize.d.ts +73 -0
- package/dist/utils/sanitize.js +254 -0
- package/dist/widget/button.d.ts +33 -0
- package/dist/widget/button.js +143 -0
- package/dist/widget/components/dom-element-cache.d.ts +62 -0
- package/dist/widget/components/dom-element-cache.js +105 -0
- package/dist/widget/components/form-validator.d.ts +66 -0
- package/dist/widget/components/form-validator.js +115 -0
- package/dist/widget/components/pii-detection-display.d.ts +64 -0
- package/dist/widget/components/pii-detection-display.js +142 -0
- package/dist/widget/components/redaction-canvas.d.ts +95 -0
- package/dist/widget/components/redaction-canvas.js +230 -0
- package/dist/widget/components/screenshot-processor.d.ts +44 -0
- package/dist/widget/components/screenshot-processor.js +191 -0
- package/dist/widget/components/style-manager.d.ts +37 -0
- package/dist/widget/components/style-manager.js +296 -0
- package/dist/widget/components/template-manager.d.ts +66 -0
- package/dist/widget/components/template-manager.js +198 -0
- package/dist/widget/modal.d.ts +62 -0
- package/dist/widget/modal.js +299 -0
- package/docs/CDN.md +213 -0
- package/docs/FRAMEWORK_INTEGRATION.md +1104 -0
- package/docs/PUBLISHING.md +550 -0
- package/docs/SESSION_REPLAY.md +381 -0
- package/package.json +90 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RedactionCanvas
|
|
3
|
+
*
|
|
4
|
+
* Responsibility: Handle canvas drawing and redaction rectangle management
|
|
5
|
+
* Follows SRP: Only handles canvas-based redaction interactions
|
|
6
|
+
*/
|
|
7
|
+
export interface RedactionRect {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}
|
|
13
|
+
export interface RedactionCanvasConfig {
|
|
14
|
+
redactionColor?: string;
|
|
15
|
+
cursorStyle?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class RedactionCanvas {
|
|
18
|
+
private canvas;
|
|
19
|
+
private ctx;
|
|
20
|
+
private isDrawing;
|
|
21
|
+
private isRedactionMode;
|
|
22
|
+
private startX;
|
|
23
|
+
private startY;
|
|
24
|
+
private redactionRects;
|
|
25
|
+
private config;
|
|
26
|
+
private handleMouseDown;
|
|
27
|
+
private handleMouseMove;
|
|
28
|
+
private handleMouseUp;
|
|
29
|
+
constructor(canvas: HTMLCanvasElement, config?: RedactionCanvasConfig);
|
|
30
|
+
/**
|
|
31
|
+
* Initialize canvas with image dimensions
|
|
32
|
+
*/
|
|
33
|
+
initializeCanvas(img: HTMLImageElement): void;
|
|
34
|
+
/**
|
|
35
|
+
* Enable redaction mode
|
|
36
|
+
*/
|
|
37
|
+
enableRedactionMode(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Disable redaction mode
|
|
40
|
+
*/
|
|
41
|
+
disableRedactionMode(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Toggle redaction mode
|
|
44
|
+
*/
|
|
45
|
+
toggleRedactionMode(): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Clear all redaction rectangles
|
|
48
|
+
*/
|
|
49
|
+
clearRedactions(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Get all redaction rectangles
|
|
52
|
+
*/
|
|
53
|
+
getRedactions(): ReadonlyArray<RedactionRect>;
|
|
54
|
+
/**
|
|
55
|
+
* Set redaction rectangles (useful for restoring state)
|
|
56
|
+
*/
|
|
57
|
+
setRedactions(rects: RedactionRect[]): void;
|
|
58
|
+
/**
|
|
59
|
+
* Check if currently in redaction mode
|
|
60
|
+
*/
|
|
61
|
+
isActive(): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Get the canvas element
|
|
64
|
+
*/
|
|
65
|
+
getCanvas(): HTMLCanvasElement;
|
|
66
|
+
/**
|
|
67
|
+
* Get canvas as data URL
|
|
68
|
+
*/
|
|
69
|
+
toDataURL(type?: string, quality?: number): string;
|
|
70
|
+
/**
|
|
71
|
+
* Hide the canvas
|
|
72
|
+
*/
|
|
73
|
+
hide(): void;
|
|
74
|
+
/**
|
|
75
|
+
* Show the canvas
|
|
76
|
+
*/
|
|
77
|
+
show(): void;
|
|
78
|
+
/**
|
|
79
|
+
* Destroy and cleanup
|
|
80
|
+
*/
|
|
81
|
+
destroy(): void;
|
|
82
|
+
private onMouseDown;
|
|
83
|
+
private onMouseMove;
|
|
84
|
+
private onMouseUp;
|
|
85
|
+
private redraw;
|
|
86
|
+
private drawRect;
|
|
87
|
+
/**
|
|
88
|
+
* Update configuration
|
|
89
|
+
*/
|
|
90
|
+
updateConfig(config: Partial<RedactionCanvasConfig>): void;
|
|
91
|
+
/**
|
|
92
|
+
* Get current configuration
|
|
93
|
+
*/
|
|
94
|
+
getConfig(): Required<RedactionCanvasConfig>;
|
|
95
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RedactionCanvas
|
|
4
|
+
*
|
|
5
|
+
* Responsibility: Handle canvas drawing and redaction rectangle management
|
|
6
|
+
* Follows SRP: Only handles canvas-based redaction interactions
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.RedactionCanvas = void 0;
|
|
10
|
+
class RedactionCanvas {
|
|
11
|
+
constructor(canvas, config = {}) {
|
|
12
|
+
this.isDrawing = false;
|
|
13
|
+
this.isRedactionMode = false;
|
|
14
|
+
this.startX = 0;
|
|
15
|
+
this.startY = 0;
|
|
16
|
+
this.redactionRects = [];
|
|
17
|
+
this.canvas = canvas;
|
|
18
|
+
const context = canvas.getContext('2d');
|
|
19
|
+
if (!context) {
|
|
20
|
+
throw new Error('Failed to get 2D context from canvas');
|
|
21
|
+
}
|
|
22
|
+
this.ctx = context;
|
|
23
|
+
this.config = {
|
|
24
|
+
redactionColor: config.redactionColor || '#000000',
|
|
25
|
+
cursorStyle: config.cursorStyle || 'crosshair',
|
|
26
|
+
};
|
|
27
|
+
// Bind event handlers
|
|
28
|
+
this.handleMouseDown = this.onMouseDown.bind(this);
|
|
29
|
+
this.handleMouseMove = this.onMouseMove.bind(this);
|
|
30
|
+
this.handleMouseUp = this.onMouseUp.bind(this);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Initialize canvas with image dimensions
|
|
34
|
+
*/
|
|
35
|
+
initializeCanvas(img) {
|
|
36
|
+
// Wait for image to load to get accurate dimensions
|
|
37
|
+
const initDimensions = () => {
|
|
38
|
+
// Get the actual rendered dimensions of the image
|
|
39
|
+
const rect = img.getBoundingClientRect();
|
|
40
|
+
const displayWidth = rect.width || img.width;
|
|
41
|
+
const displayHeight = rect.height || img.height;
|
|
42
|
+
// Set canvas internal dimensions to match natural image size for high resolution
|
|
43
|
+
this.canvas.width = img.naturalWidth || img.width;
|
|
44
|
+
this.canvas.height = img.naturalHeight || img.height;
|
|
45
|
+
// Set canvas display size to match the rendered image size
|
|
46
|
+
this.canvas.style.width = `${displayWidth}px`;
|
|
47
|
+
this.canvas.style.height = `${displayHeight}px`;
|
|
48
|
+
};
|
|
49
|
+
if (img.complete && img.naturalWidth > 0) {
|
|
50
|
+
initDimensions();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
img.addEventListener('load', initDimensions, { once: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Enable redaction mode
|
|
58
|
+
*/
|
|
59
|
+
enableRedactionMode() {
|
|
60
|
+
if (this.isRedactionMode) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.isRedactionMode = true;
|
|
64
|
+
this.canvas.style.display = 'block';
|
|
65
|
+
this.canvas.style.cursor = this.config.cursorStyle;
|
|
66
|
+
// Attach event listeners
|
|
67
|
+
this.canvas.addEventListener('mousedown', this.handleMouseDown);
|
|
68
|
+
this.canvas.addEventListener('mousemove', this.handleMouseMove);
|
|
69
|
+
this.canvas.addEventListener('mouseup', this.handleMouseUp);
|
|
70
|
+
this.redraw();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Disable redaction mode
|
|
74
|
+
*/
|
|
75
|
+
disableRedactionMode() {
|
|
76
|
+
if (!this.isRedactionMode) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.isRedactionMode = false;
|
|
80
|
+
this.isDrawing = false;
|
|
81
|
+
// Remove event listeners
|
|
82
|
+
this.canvas.removeEventListener('mousedown', this.handleMouseDown);
|
|
83
|
+
this.canvas.removeEventListener('mousemove', this.handleMouseMove);
|
|
84
|
+
this.canvas.removeEventListener('mouseup', this.handleMouseUp);
|
|
85
|
+
this.canvas.style.cursor = 'default';
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Toggle redaction mode
|
|
89
|
+
*/
|
|
90
|
+
toggleRedactionMode() {
|
|
91
|
+
if (this.isRedactionMode) {
|
|
92
|
+
this.disableRedactionMode();
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.enableRedactionMode();
|
|
96
|
+
}
|
|
97
|
+
return this.isRedactionMode;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Clear all redaction rectangles
|
|
101
|
+
*/
|
|
102
|
+
clearRedactions() {
|
|
103
|
+
this.redactionRects = [];
|
|
104
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get all redaction rectangles
|
|
108
|
+
*/
|
|
109
|
+
getRedactions() {
|
|
110
|
+
return [...this.redactionRects];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Set redaction rectangles (useful for restoring state)
|
|
114
|
+
*/
|
|
115
|
+
setRedactions(rects) {
|
|
116
|
+
this.redactionRects = [...rects];
|
|
117
|
+
this.redraw();
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Check if currently in redaction mode
|
|
121
|
+
*/
|
|
122
|
+
isActive() {
|
|
123
|
+
return this.isRedactionMode;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get the canvas element
|
|
127
|
+
*/
|
|
128
|
+
getCanvas() {
|
|
129
|
+
return this.canvas;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get canvas as data URL
|
|
133
|
+
*/
|
|
134
|
+
toDataURL(type = 'image/png', quality) {
|
|
135
|
+
return this.canvas.toDataURL(type, quality);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Hide the canvas
|
|
139
|
+
*/
|
|
140
|
+
hide() {
|
|
141
|
+
this.canvas.style.display = 'none';
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Show the canvas
|
|
145
|
+
*/
|
|
146
|
+
show() {
|
|
147
|
+
this.canvas.style.display = 'block';
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Destroy and cleanup
|
|
151
|
+
*/
|
|
152
|
+
destroy() {
|
|
153
|
+
this.disableRedactionMode();
|
|
154
|
+
this.clearRedactions();
|
|
155
|
+
}
|
|
156
|
+
// Private event handlers
|
|
157
|
+
onMouseDown(e) {
|
|
158
|
+
if (!this.isRedactionMode) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
162
|
+
const scaleX = this.canvas.width / rect.width;
|
|
163
|
+
const scaleY = this.canvas.height / rect.height;
|
|
164
|
+
this.startX = (e.clientX - rect.left) * scaleX;
|
|
165
|
+
this.startY = (e.clientY - rect.top) * scaleY;
|
|
166
|
+
this.isDrawing = true;
|
|
167
|
+
}
|
|
168
|
+
onMouseMove(e) {
|
|
169
|
+
if (!this.isDrawing || !this.isRedactionMode) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
173
|
+
const scaleX = this.canvas.width / rect.width;
|
|
174
|
+
const scaleY = this.canvas.height / rect.height;
|
|
175
|
+
const currentX = (e.clientX - rect.left) * scaleX;
|
|
176
|
+
const currentY = (e.clientY - rect.top) * scaleY;
|
|
177
|
+
this.redraw();
|
|
178
|
+
this.drawRect(this.startX, this.startY, currentX - this.startX, currentY - this.startY);
|
|
179
|
+
}
|
|
180
|
+
onMouseUp(e) {
|
|
181
|
+
if (!this.isDrawing || !this.isRedactionMode) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
185
|
+
const scaleX = this.canvas.width / rect.width;
|
|
186
|
+
const scaleY = this.canvas.height / rect.height;
|
|
187
|
+
const endX = (e.clientX - rect.left) * scaleX;
|
|
188
|
+
const endY = (e.clientY - rect.top) * scaleY;
|
|
189
|
+
const width = endX - this.startX;
|
|
190
|
+
const height = endY - this.startY;
|
|
191
|
+
// Only add rectangle if it has meaningful size
|
|
192
|
+
if (Math.abs(width) > 5 && Math.abs(height) > 5) {
|
|
193
|
+
this.redactionRects.push({
|
|
194
|
+
x: this.startX,
|
|
195
|
+
y: this.startY,
|
|
196
|
+
width,
|
|
197
|
+
height,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
this.isDrawing = false;
|
|
201
|
+
this.redraw();
|
|
202
|
+
}
|
|
203
|
+
// Private drawing methods
|
|
204
|
+
redraw() {
|
|
205
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
206
|
+
for (const rect of this.redactionRects) {
|
|
207
|
+
this.drawRect(rect.x, rect.y, rect.width, rect.height);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
drawRect(x, y, width, height) {
|
|
211
|
+
this.ctx.fillStyle = this.config.redactionColor;
|
|
212
|
+
this.ctx.fillRect(x, y, width, height);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Update configuration
|
|
216
|
+
*/
|
|
217
|
+
updateConfig(config) {
|
|
218
|
+
this.config = Object.assign(Object.assign({}, this.config), config);
|
|
219
|
+
if (this.isRedactionMode) {
|
|
220
|
+
this.canvas.style.cursor = this.config.cursorStyle;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Get current configuration
|
|
225
|
+
*/
|
|
226
|
+
getConfig() {
|
|
227
|
+
return Object.assign({}, this.config);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
exports.RedactionCanvas = RedactionCanvas;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScreenshotProcessor
|
|
3
|
+
*
|
|
4
|
+
* Responsibility: Handle screenshot image processing and manipulation
|
|
5
|
+
* Follows SRP: Only handles image-related operations
|
|
6
|
+
*/
|
|
7
|
+
import type { RedactionRect } from './redaction-canvas';
|
|
8
|
+
export declare class ScreenshotProcessor {
|
|
9
|
+
/**
|
|
10
|
+
* Merge redaction canvas with original screenshot
|
|
11
|
+
*/
|
|
12
|
+
mergeRedactions(originalDataUrl: string, redactionCanvas: HTMLCanvasElement): Promise<string>;
|
|
13
|
+
/**
|
|
14
|
+
* Apply redaction rectangles directly to an image data URL
|
|
15
|
+
*/
|
|
16
|
+
applyRedactions(imageDataUrl: string, redactions: RedactionRect[], redactionColor?: string): Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Resize an image to maximum dimensions while maintaining aspect ratio
|
|
19
|
+
*/
|
|
20
|
+
resize(imageDataUrl: string, maxWidth: number, maxHeight: number): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Convert image to different format
|
|
23
|
+
*/
|
|
24
|
+
convert(imageDataUrl: string, format: 'image/png' | 'image/jpeg' | 'image/webp', quality?: number): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Get image dimensions from data URL
|
|
27
|
+
*/
|
|
28
|
+
getDimensions(imageDataUrl: string): Promise<{
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Validate if a string is a valid data URL
|
|
34
|
+
*/
|
|
35
|
+
isValidDataURL(dataUrl: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Get file size estimate from data URL (in bytes)
|
|
38
|
+
*/
|
|
39
|
+
getEstimatedSize(dataUrl: string): number;
|
|
40
|
+
/**
|
|
41
|
+
* Get format from data URL
|
|
42
|
+
*/
|
|
43
|
+
getFormat(dataUrl: string): string | null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ScreenshotProcessor
|
|
4
|
+
*
|
|
5
|
+
* Responsibility: Handle screenshot image processing and manipulation
|
|
6
|
+
* Follows SRP: Only handles image-related operations
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ScreenshotProcessor = void 0;
|
|
10
|
+
class ScreenshotProcessor {
|
|
11
|
+
/**
|
|
12
|
+
* Merge redaction canvas with original screenshot
|
|
13
|
+
*/
|
|
14
|
+
async mergeRedactions(originalDataUrl, redactionCanvas) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const img = new Image();
|
|
17
|
+
img.onload = () => {
|
|
18
|
+
try {
|
|
19
|
+
const mergedCanvas = document.createElement('canvas');
|
|
20
|
+
mergedCanvas.width = img.naturalWidth || img.width;
|
|
21
|
+
mergedCanvas.height = img.naturalHeight || img.height;
|
|
22
|
+
const ctx = mergedCanvas.getContext('2d');
|
|
23
|
+
if (!ctx) {
|
|
24
|
+
reject(new Error('Failed to get canvas context'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// Draw original image
|
|
28
|
+
ctx.drawImage(img, 0, 0);
|
|
29
|
+
// Draw redaction canvas on top
|
|
30
|
+
ctx.drawImage(redactionCanvas, 0, 0);
|
|
31
|
+
resolve(mergedCanvas.toDataURL());
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
reject(error);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
img.onerror = () => {
|
|
38
|
+
reject(new Error('Failed to load screenshot image'));
|
|
39
|
+
};
|
|
40
|
+
img.src = originalDataUrl;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Apply redaction rectangles directly to an image data URL
|
|
45
|
+
*/
|
|
46
|
+
async applyRedactions(imageDataUrl, redactions, redactionColor = '#000000') {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const img = new Image();
|
|
49
|
+
img.onload = () => {
|
|
50
|
+
try {
|
|
51
|
+
const canvas = document.createElement('canvas');
|
|
52
|
+
canvas.width = img.naturalWidth || img.width;
|
|
53
|
+
canvas.height = img.naturalHeight || img.height;
|
|
54
|
+
const ctx = canvas.getContext('2d');
|
|
55
|
+
if (!ctx) {
|
|
56
|
+
reject(new Error('Failed to get canvas context'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Draw original image
|
|
60
|
+
ctx.drawImage(img, 0, 0);
|
|
61
|
+
// Apply redactions
|
|
62
|
+
ctx.fillStyle = redactionColor;
|
|
63
|
+
for (const rect of redactions) {
|
|
64
|
+
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
65
|
+
}
|
|
66
|
+
resolve(canvas.toDataURL());
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
reject(error);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
img.onerror = () => {
|
|
73
|
+
reject(new Error('Failed to load image'));
|
|
74
|
+
};
|
|
75
|
+
img.src = imageDataUrl;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resize an image to maximum dimensions while maintaining aspect ratio
|
|
80
|
+
*/
|
|
81
|
+
async resize(imageDataUrl, maxWidth, maxHeight) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const img = new Image();
|
|
84
|
+
img.onload = () => {
|
|
85
|
+
try {
|
|
86
|
+
let width = img.naturalWidth || img.width;
|
|
87
|
+
let height = img.naturalHeight || img.height;
|
|
88
|
+
// Calculate new dimensions maintaining aspect ratio
|
|
89
|
+
if (width > maxWidth || height > maxHeight) {
|
|
90
|
+
const aspectRatio = width / height;
|
|
91
|
+
if (width > height) {
|
|
92
|
+
width = maxWidth;
|
|
93
|
+
height = width / aspectRatio;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
height = maxHeight;
|
|
97
|
+
width = height * aspectRatio;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const canvas = document.createElement('canvas');
|
|
101
|
+
canvas.width = width;
|
|
102
|
+
canvas.height = height;
|
|
103
|
+
const ctx = canvas.getContext('2d');
|
|
104
|
+
if (!ctx) {
|
|
105
|
+
reject(new Error('Failed to get canvas context'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
109
|
+
resolve(canvas.toDataURL());
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
reject(error);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
img.onerror = () => {
|
|
116
|
+
reject(new Error('Failed to load image'));
|
|
117
|
+
};
|
|
118
|
+
img.src = imageDataUrl;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Convert image to different format
|
|
123
|
+
*/
|
|
124
|
+
async convert(imageDataUrl, format, quality = 0.92) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const img = new Image();
|
|
127
|
+
img.onload = () => {
|
|
128
|
+
try {
|
|
129
|
+
const canvas = document.createElement('canvas');
|
|
130
|
+
canvas.width = img.naturalWidth || img.width;
|
|
131
|
+
canvas.height = img.naturalHeight || img.height;
|
|
132
|
+
const ctx = canvas.getContext('2d');
|
|
133
|
+
if (!ctx) {
|
|
134
|
+
reject(new Error('Failed to get canvas context'));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
ctx.drawImage(img, 0, 0);
|
|
138
|
+
resolve(canvas.toDataURL(format, quality));
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
reject(error);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
img.onerror = () => {
|
|
145
|
+
reject(new Error('Failed to load image'));
|
|
146
|
+
};
|
|
147
|
+
img.src = imageDataUrl;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get image dimensions from data URL
|
|
152
|
+
*/
|
|
153
|
+
async getDimensions(imageDataUrl) {
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const img = new Image();
|
|
156
|
+
img.onload = () => {
|
|
157
|
+
resolve({
|
|
158
|
+
width: img.naturalWidth || img.width,
|
|
159
|
+
height: img.naturalHeight || img.height,
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
img.onerror = () => {
|
|
163
|
+
reject(new Error('Failed to load image'));
|
|
164
|
+
};
|
|
165
|
+
img.src = imageDataUrl;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Validate if a string is a valid data URL
|
|
170
|
+
*/
|
|
171
|
+
isValidDataURL(dataUrl) {
|
|
172
|
+
return /^data:image\/(png|jpeg|jpg|webp|gif);base64,/.test(dataUrl);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get file size estimate from data URL (in bytes)
|
|
176
|
+
*/
|
|
177
|
+
getEstimatedSize(dataUrl) {
|
|
178
|
+
// Base64 encoding increases size by ~33%
|
|
179
|
+
// Remove the data URL prefix to get just the base64 data
|
|
180
|
+
const base64Data = dataUrl.split(',')[1] || '';
|
|
181
|
+
return Math.ceil((base64Data.length * 3) / 4);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get format from data URL
|
|
185
|
+
*/
|
|
186
|
+
getFormat(dataUrl) {
|
|
187
|
+
const match = dataUrl.match(/^data:image\/([a-z]+);base64,/);
|
|
188
|
+
return match ? match[1] : null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
exports.ScreenshotProcessor = ScreenshotProcessor;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StyleManager
|
|
3
|
+
*
|
|
4
|
+
* Responsibility: Generate and manage CSS styles for the bug report modal
|
|
5
|
+
* Follows SRP: Only handles style generation and theming
|
|
6
|
+
*/
|
|
7
|
+
export interface StyleConfig {
|
|
8
|
+
primaryColor?: string;
|
|
9
|
+
dangerColor?: string;
|
|
10
|
+
borderRadius?: string;
|
|
11
|
+
fontFamily?: string;
|
|
12
|
+
zIndex?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class StyleManager {
|
|
15
|
+
private config;
|
|
16
|
+
constructor(config?: StyleConfig);
|
|
17
|
+
/**
|
|
18
|
+
* Generate complete CSS stylesheet for the modal
|
|
19
|
+
*/
|
|
20
|
+
generateStyles(): string;
|
|
21
|
+
/**
|
|
22
|
+
* Inject styles into document head
|
|
23
|
+
*/
|
|
24
|
+
injectStyles(): HTMLStyleElement;
|
|
25
|
+
/**
|
|
26
|
+
* Remove injected styles
|
|
27
|
+
*/
|
|
28
|
+
removeStyles(styleElement: HTMLStyleElement): void;
|
|
29
|
+
/**
|
|
30
|
+
* Update configuration and regenerate styles
|
|
31
|
+
*/
|
|
32
|
+
updateConfig(config: Partial<StyleConfig>): void;
|
|
33
|
+
/**
|
|
34
|
+
* Get current configuration
|
|
35
|
+
*/
|
|
36
|
+
getConfig(): Required<StyleConfig>;
|
|
37
|
+
}
|