JiraFeedback/dist/popup.js
2025-01-26 18:08:50 +05:30

1016 lines
35 KiB
JavaScript

/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ 169:
/***/ (() => {
class ScreenshotEditor {
constructor(canvas) {
console.log('Initializing ScreenshotEditor');
this.canvas = canvas;
this.ctx = canvas.getContext('2d', {
alpha: false
});
this.isDrawing = false;
this.drawingMode = 'pen';
this.drawColor = '#ff0000';
this.lineWidth = 2;
this.startX = 0;
this.startY = 0;
this.lastX = 0;
this.lastY = 0;
this.undoStack = [];
this.redoStack = [];
this.originalImage = null;
this.currentState = null;
this.scale = 1;
this.paths = [];
this.currentPath = [];
this.cropStart = null;
this.cropEnd = null;
this.setupEventListeners();
this.setupTools();
console.log('ScreenshotEditor initialized');
}
async loadImage(dataUrl) {
console.log('Loading image...');
return new Promise((resolve, reject) => {
if (!dataUrl || typeof dataUrl !== 'string') {
console.error('Invalid dataUrl:', dataUrl);
reject(new Error('Invalid image data'));
return;
}
const img = new Image();
img.onload = () => {
console.log('Image loaded, dimensions:', img.width, 'x', img.height);
this.originalImage = img;
// Use device pixel ratio for better resolution
const dpr = window.devicePixelRatio || 1;
// Calculate dimensions to maintain aspect ratio and high resolution
const maxWidth = Math.min(window.innerWidth * 0.98, img.width); // 98% of window width or original width
const maxHeight = Math.min(window.innerHeight * 0.9, img.height); // 90% of window height or original height
const aspectRatio = img.width / img.height;
let newWidth = img.width;
let newHeight = img.height;
// Scale down if necessary while maintaining aspect ratio
if (newWidth > maxWidth || newHeight > maxHeight) {
if (maxWidth / aspectRatio <= maxHeight) {
newWidth = maxWidth;
newHeight = maxWidth / aspectRatio;
} else {
newHeight = maxHeight;
newWidth = maxHeight * aspectRatio;
}
}
// Set canvas size accounting for device pixel ratio
this.canvas.style.width = newWidth + 'px';
this.canvas.style.height = newHeight + 'px';
this.canvas.width = newWidth * dpr;
this.canvas.height = newHeight * dpr;
// Scale context for high DPI display
this.ctx.scale(dpr, dpr);
// Calculate scale for proper coordinate mapping
this.scale = newWidth / img.width;
// Enable image smoothing
this.ctx.imageSmoothingEnabled = true;
this.ctx.imageSmoothingQuality = 'high';
// Clear canvas and draw image with crisp rendering
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(img, 0, 0, newWidth, newHeight);
// Save initial state
this.saveState();
resolve();
};
img.onerror = error => {
console.error('Error loading image:', error);
reject(new Error('Failed to load image'));
};
console.log('Setting image source...');
img.src = dataUrl;
});
}
setupEventListeners() {
this.canvas.addEventListener('mousedown', this.startDrawing.bind(this));
this.canvas.addEventListener('mousemove', this.draw.bind(this));
this.canvas.addEventListener('mouseup', this.stopDrawing.bind(this));
this.canvas.addEventListener('mouseleave', this.stopDrawing.bind(this));
// Setup keyboard shortcuts
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.key === 'z') {
this.undo();
} else if (e.ctrlKey && e.key === 'y') {
this.redo();
}
});
}
setupTools() {
const tools = ['pen', 'rectangle', 'arrow', 'crop'];
tools.forEach(tool => {
const button = document.getElementById(`${tool}-tool`);
if (button) {
button.addEventListener('click', () => {
this.setDrawingMode(tool);
// Remove active class from all tools
tools.forEach(t => {
var _document$getElementB;
(_document$getElementB = document.getElementById(`${t}-tool`)) === null || _document$getElementB === void 0 || _document$getElementB.classList.remove('active');
});
// Add active class to selected tool
button.classList.add('active');
});
}
});
// Setup color picker
const colorPicker = document.getElementById('color-picker');
if (colorPicker) {
colorPicker.addEventListener('change', e => {
this.drawColor = e.target.value;
});
}
// Setup line width
const lineWidth = document.getElementById('line-width');
if (lineWidth) {
lineWidth.addEventListener('change', e => {
this.lineWidth = parseInt(e.target.value);
});
}
// Setup undo/redo buttons
const undoBtn = document.getElementById('undo-btn');
const redoBtn = document.getElementById('redo-btn');
if (undoBtn) undoBtn.addEventListener('click', () => this.undo());
if (redoBtn) redoBtn.addEventListener('click', () => this.redo());
}
startDrawing(e) {
const rect = this.canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
this.isDrawing = true;
// Calculate coordinates taking into account scale and DPR
const x = (e.clientX - rect.left) / rect.width * this.canvas.width / dpr;
const y = (e.clientY - rect.top) / rect.height * this.canvas.height / dpr;
this.startX = x;
this.startY = y;
this.lastX = x;
this.lastY = y;
if (this.drawingMode === 'crop') {
this.cropStart = {
x,
y
};
this.cropEnd = null;
} else if (this.drawingMode === 'pen') {
this.currentPath = [];
this.currentPath.push({
x,
y
});
this.paths.push(this.currentPath);
}
}
draw(e) {
if (!this.isDrawing) return;
const rect = this.canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const currentX = (e.clientX - rect.left) / rect.width * this.canvas.width / dpr;
const currentY = (e.clientY - rect.top) / rect.height * this.canvas.height / dpr;
// Clear the canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Draw the current state
if (this.currentState) {
const img = new Image();
img.src = this.currentState;
this.ctx.drawImage(img, 0, 0, this.canvas.width / dpr, this.canvas.height / dpr);
} else {
this.ctx.drawImage(this.originalImage, 0, 0, this.canvas.width / dpr, this.canvas.height / dpr);
}
// Set drawing styles
this.ctx.strokeStyle = this.drawColor;
this.ctx.lineWidth = this.lineWidth;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
switch (this.drawingMode) {
case 'pen':
this.currentPath.push({
x: currentX,
y: currentY
});
this.ctx.beginPath();
this.ctx.moveTo(this.currentPath[0].x, this.currentPath[0].y);
for (let i = 1; i < this.currentPath.length; i++) {
const point = this.currentPath[i];
this.ctx.lineTo(point.x, point.y);
}
this.ctx.stroke();
break;
case 'rectangle':
this.ctx.beginPath();
this.ctx.rect(this.startX, this.startY, currentX - this.startX, currentY - this.startY);
this.ctx.stroke();
break;
case 'arrow':
this.drawArrow(this.ctx, this.startX, this.startY, currentX, currentY);
break;
case 'crop':
// Draw crop rectangle
this.ctx.save();
this.ctx.strokeStyle = '#ffffff';
this.ctx.lineWidth = 2;
this.ctx.setLineDash([5, 5]);
this.ctx.beginPath();
this.ctx.rect(this.startX, this.startY, currentX - this.startX, currentY - this.startY);
this.ctx.stroke();
// Draw semi-transparent overlay
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
this.ctx.fillRect(0, 0, this.canvas.width, this.startY); // Top
this.ctx.fillRect(0, currentY, this.canvas.width, this.canvas.height - currentY); // Bottom
this.ctx.fillRect(0, this.startY, this.startX, currentY - this.startY); // Left
this.ctx.fillRect(currentX, this.startY, this.canvas.width - currentX, currentY - this.startY); // Right
this.ctx.restore();
this.cropEnd = {
x: currentX,
y: currentY
};
break;
}
}
drawArrow(ctx, fromX, fromY, toX, toY) {
const headLength = 20;
const angle = Math.atan2(toY - fromY, toX - fromX);
// Draw the line
ctx.beginPath();
ctx.moveTo(fromX, fromY);
ctx.lineTo(toX, toY);
ctx.stroke();
// Draw the arrow head
ctx.beginPath();
ctx.moveTo(toX, toY);
ctx.lineTo(toX - headLength * Math.cos(angle - Math.PI / 6), toY - headLength * Math.sin(angle - Math.PI / 6));
ctx.moveTo(toX, toY);
ctx.lineTo(toX - headLength * Math.cos(angle + Math.PI / 6), toY - headLength * Math.sin(angle + Math.PI / 6));
ctx.stroke();
}
stopDrawing() {
if (!this.isDrawing) return;
this.isDrawing = false;
if (this.drawingMode === 'crop' && this.cropStart && this.cropEnd) {
// Ensure positive width and height
const x = Math.min(this.cropStart.x, this.cropEnd.x);
const y = Math.min(this.cropStart.y, this.cropEnd.y);
const width = Math.abs(this.cropEnd.x - this.cropStart.x);
const height = Math.abs(this.cropEnd.y - this.cropStart.y);
// Create a temporary canvas for cropping
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = width;
tempCanvas.height = height;
// Draw the cropped portion
if (this.currentState) {
const img = new Image();
img.onload = () => {
tempCtx.drawImage(img, -x, -y);
// Update canvas size
this.canvas.width = width;
this.canvas.height = height;
// Draw cropped image
this.ctx.drawImage(tempCanvas, 0, 0);
// Save the cropped state
this.currentState = this.canvas.toDataURL('image/png');
this.saveState();
};
img.src = this.currentState;
} else if (this.originalImage) {
tempCtx.drawImage(this.originalImage, -x, -y);
// Update canvas size
this.canvas.width = width;
this.canvas.height = height;
// Draw cropped image
this.ctx.drawImage(tempCanvas, 0, 0);
// Save the cropped state
this.currentState = this.canvas.toDataURL('image/png');
this.saveState();
}
// Reset crop points
this.cropStart = null;
this.cropEnd = null;
} else {
// For other tools, save the current state
this.currentState = this.canvas.toDataURL('image/png');
this.saveState();
}
// Reset current path
this.currentPath = [];
}
undo() {
if (this.undoStack.length > 1) {
this.redoStack.push(this.undoStack.pop()); // Move current state to redo stack
const previousState = this.undoStack[this.undoStack.length - 1];
this.loadStateImage(previousState);
}
}
redo() {
if (this.redoStack.length > 0) {
const nextState = this.redoStack.pop();
this.undoStack.push(nextState);
this.loadStateImage(nextState);
}
}
loadStateImage(dataUrl) {
const img = new Image();
const dpr = window.devicePixelRatio || 1;
img.onload = () => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(img, 0, 0, this.canvas.width / dpr, this.canvas.height / dpr);
this.currentState = dataUrl;
};
img.src = dataUrl;
}
getImageData() {
// Return the current state with drawings
return this.currentState || this.canvas.toDataURL('image/png');
}
setDrawingMode(mode) {
this.drawingMode = mode;
this.canvas.style.cursor = mode === 'crop' ? 'crosshair' : 'default';
}
saveState() {
const imageData = this.canvas.toDataURL('image/png');
this.undoStack.push(imageData);
this.redoStack = []; // Clear redo stack when new state is saved
}
redrawCanvas() {
if (!this.canvas.width || !this.canvas.height) {
console.warn('Canvas has no dimensions, skipping redraw');
return;
}
const dpr = window.devicePixelRatio || 1;
// Clear the canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Draw the current state if it exists, otherwise draw the original image
if (this.currentState) {
const img = new Image();
img.src = this.currentState;
this.ctx.drawImage(img, 0, 0, this.canvas.width / dpr, this.canvas.height / dpr);
} else if (this.originalImage) {
this.ctx.drawImage(this.originalImage, 0, 0, this.canvas.width / dpr, this.canvas.height / dpr);
}
}
}
window.ScreenshotEditor = ScreenshotEditor;
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/************************************************************************/
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
(() => {
"use strict";
/* harmony import */ var _screenshot_editor_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(169);
/* harmony import */ var _screenshot_editor_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_screenshot_editor_js__WEBPACK_IMPORTED_MODULE_0__);
class FeedbackExtension {
constructor() {
this.screenshot = null;
this.editorWindow = null;
this.mediaRecorder = null;
this.recordedChunks = [];
this.recordingDialog = null;
this.recordedVideo = null;
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
this.initializeElements();
this.attachEventListeners();
this.loadSettings();
});
// Listen for messages from the editor window
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Popup received message:', message);
try {
if (message.type === 'screenshot-saved') {
this.screenshot = message.screenshot;
this.attachmentPreview.innerHTML = `
<div class="screenshot-preview">
<img src="${this.screenshot}" alt="Screenshot" style="max-width: 100%; height: auto;">
</div>
`;
// Close the editor window if it exists
if (this.editorWindow && !this.editorWindow.closed) {
this.editorWindow.close();
}
sendResponse({
success: true
});
} else if (message.type === 'editor-ready') {
console.log('Editor is ready, sending screenshot...');
if (this.screenshot) {
chrome.runtime.sendMessage({
type: 'init-editor',
screenshot: this.screenshot
}, response => {
console.log('Screenshot sent to editor, response:', response);
});
}
sendResponse({
success: true
});
}
} catch (error) {
console.error('Error handling message:', error);
sendResponse({
success: false,
error: error.message
});
}
return true; // Keep the message channel open for async response
});
}
initializeElements() {
// Form elements
this.feedbackForm = document.getElementById('feedback-form');
this.settingsPanel = document.getElementById('settings-panel');
this.screenshotBtn = document.getElementById('screenshot-btn');
this.recordBtn = document.getElementById('record-btn');
this.submitFeedbackBtn = document.getElementById('submit-feedback');
this.settingsBtn = document.getElementById('settings-btn');
this.saveSettingsBtn = document.getElementById('save-settings');
this.attachmentPreview = document.getElementById('attachment-preview');
// Settings inputs
this.jiraDomainInput = document.getElementById('jira-domain');
this.jiraEmailInput = document.getElementById('jira-email');
this.jiraTokenInput = document.getElementById('jira-token');
this.jiraProjectInput = document.getElementById('jira-project');
// Notification
this.notification = document.getElementById('notification');
// Create recording dialog
this.recordingDialog = document.createElement('div');
this.recordingDialog.className = 'recording-dialog hidden';
this.recordingDialog.innerHTML = `
<div class="recording-content">
<div class="recording-status">Recording...</div>
<button id="stop-recording" class="primary-button">Stop Recording</button>
</div>
`;
document.body.appendChild(this.recordingDialog);
}
attachEventListeners() {
if (this.screenshotBtn) {
this.screenshotBtn.addEventListener('click', () => this.captureScreenshot());
}
if (this.recordBtn) {
this.recordBtn.addEventListener('click', () => this.startScreenRecording());
}
if (this.submitFeedbackBtn) {
this.submitFeedbackBtn.addEventListener('click', () => this.handleSubmit());
}
if (this.settingsBtn) {
this.settingsBtn.addEventListener('click', () => this.showSettings());
}
if (this.saveSettingsBtn) {
this.saveSettingsBtn.addEventListener('click', () => this.saveSettings());
}
}
async loadSettings() {
const settings = await chrome.storage.local.get(['jiraDomain', 'jiraEmail', 'jiraToken', 'jiraProject']);
// Fill in settings if they exist
if (settings.jiraDomain) this.jiraDomainInput.value = settings.jiraDomain;
if (settings.jiraEmail) this.jiraEmailInput.value = settings.jiraEmail;
if (settings.jiraToken) this.jiraTokenInput.value = settings.jiraToken;
if (settings.jiraProject) this.jiraProjectInput.value = settings.jiraProject;
// Show appropriate panel based on whether settings exist
if (settings.jiraDomain && settings.jiraEmail && settings.jiraToken && settings.jiraProject) {
this.showFeedbackForm();
} else {
this.showSettings();
}
}
async saveSettings() {
const domain = this.jiraDomainInput.value.trim();
const email = this.jiraEmailInput.value.trim();
const token = this.jiraTokenInput.value.trim();
const project = this.jiraProjectInput.value.trim();
if (!domain || !email || !token || !project) {
this.showNotification('Please fill in all fields', 'error');
return;
}
try {
// Validate credentials by making a test API call
const isValid = await this.validateJiraCredentials(domain, email, token);
if (!isValid) {
this.showNotification('Invalid Jira credentials', 'error');
return;
}
// Save settings
await chrome.storage.local.set({
jiraDomain: domain,
jiraEmail: email,
jiraToken: token,
jiraProject: project
});
this.showNotification('Settings saved successfully', 'success');
this.showFeedbackForm();
} catch (error) {
console.error('Error saving settings:', error);
this.showNotification('Error saving settings: ' + error.message, 'error');
}
}
async validateJiraCredentials(domain, email, token) {
try {
const response = await fetch(`https://${domain}/rest/api/2/myself`, {
method: 'GET',
headers: {
'Authorization': `Basic ${btoa(`${email}:${token}`)}`,
'Accept': 'application/json'
}
});
return response.ok;
} catch (error) {
console.error('Error validating Jira credentials:', error);
return false;
}
}
async handleSubmit() {
try {
const settings = await chrome.storage.local.get(['jiraDomain', 'jiraEmail', 'jiraToken', 'jiraProject']);
if (!settings.jiraDomain || !settings.jiraEmail || !settings.jiraToken || !settings.jiraProject) {
this.showNotification('Please configure Jira settings first', 'error');
this.showSettings();
return;
}
const type = document.getElementById('feedback-type').value;
const title = document.getElementById('feedback-title').value;
const description = document.getElementById('feedback-description').value;
if (!title || !description) {
this.showNotification('Please fill in all required fields', 'error');
return;
}
// Map feedback type to standard Jira issue types
let issueType;
switch (type) {
case 'Bug':
issueType = 'Bug';
break;
case 'Feature':
issueType = 'Story';
break;
case 'Improvement':
issueType = 'Task';
break;
default:
issueType = 'Task';
}
// Get current tab URL for context
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
});
const contextInfo = `
Reported from: ${tab.url}
Browser: ${navigator.userAgent}
Timestamp: ${new Date().toISOString()}
Description:
${description}
`;
const formData = {
fields: {
project: {
key: settings.jiraProject
},
summary: title,
description: contextInfo,
issuetype: {
name: issueType
},
labels: ["feedback-extension"]
}
};
console.log('Submitting to Jira with data:', JSON.stringify(formData, null, 2));
const response = await this.submitToJira(formData);
// If we have a screenshot or video, attach it to the created issue
if (response.key) {
if (this.screenshot) {
await this.attachScreenshotToIssue(response.key, this.screenshot);
}
if (this.recordedVideo) {
await this.attachVideoToIssue(response.key, this.recordedVideo);
}
}
if (response.key) {
this.showNotification(`Issue ${response.key} created successfully!`, 'success');
document.getElementById('feedback-title').value = '';
document.getElementById('feedback-description').value = '';
this.attachmentPreview.innerHTML = '';
this.screenshot = null;
this.recordedVideo = null;
}
} catch (error) {
console.error('Full error details:', error);
this.showNotification('Error submitting feedback: ' + error.message, 'error');
}
}
async submitToJira(formData) {
const settings = await chrome.storage.local.get(['jiraDomain', 'jiraEmail', 'jiraToken']);
try {
// First, get available issue types for the project
const metaResponse = await fetch(`https://${settings.jiraDomain}/rest/api/2/issue/createmeta?projectKeys=${formData.fields.project.key}&expand=projects.issuetypes.fields`, {
method: 'GET',
headers: {
'Authorization': `Basic ${btoa(`${settings.jiraEmail}:${settings.jiraToken}`)}`,
'Accept': 'application/json'
}
});
const metaData = await metaResponse.json();
console.log('Project metadata:', metaData);
if (!metaResponse.ok) {
throw new Error('Failed to get project metadata');
}
// Get available issue types
const project = metaData.projects[0];
if (!project) {
throw new Error('Project not found or no access');
}
const availableTypes = project.issuetypes.map(type => type.name);
console.log('Available issue types:', availableTypes);
// If the desired issue type isn't available, fall back to Task
if (!availableTypes.includes(formData.fields.issuetype.name)) {
console.log(`Issue type ${formData.fields.issuetype.name} not available, falling back to Task`);
formData.fields.issuetype.name = 'Task';
}
// Create the issue
const response = await fetch(`https://${settings.jiraDomain}/rest/api/2/issue`, {
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`${settings.jiraEmail}:${settings.jiraToken}`)}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const responseData = await response.json();
console.log('Jira API Response:', responseData);
if (!response.ok) {
if (responseData.errors) {
const errorMessage = Object.entries(responseData.errors).map(([field, error]) => `${field}: ${error}`).join(', ');
throw new Error(errorMessage);
} else if (responseData.errorMessages) {
throw new Error(responseData.errorMessages.join(', '));
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
}
return responseData;
} catch (error) {
console.error('Error submitting to Jira:', error);
console.error('Request data:', JSON.stringify(formData, null, 2));
throw error;
}
}
async attachScreenshotToIssue(issueKey, screenshotDataUrl) {
try {
const settings = await chrome.storage.local.get(['jiraDomain', 'jiraEmail', 'jiraToken']);
// Convert base64 data URL to blob
const base64Data = screenshotDataUrl.replace(/^data:image\/png;base64,/, '');
const byteCharacters = atob(base64Data);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
const slice = byteCharacters.slice(offset, offset + 512);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
const blob = new Blob(byteArrays, {
type: 'image/png'
});
// Create form data with the screenshot
const formData = new FormData();
formData.append('file', blob, 'screenshot.png');
// Upload the attachment
const response = await fetch(`https://${settings.jiraDomain}/rest/api/2/issue/${issueKey}/attachments`, {
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`${settings.jiraEmail}:${settings.jiraToken}`)}`,
'X-Atlassian-Token': 'no-check' // Required for file uploads
},
body: formData
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to attach screenshot: ${error}`);
}
console.log('Screenshot attached successfully');
} catch (error) {
console.error('Error attaching screenshot:', error);
throw new Error('Failed to attach screenshot: ' + error.message);
}
}
async attachVideoToIssue(issueKey, videoBlob) {
try {
const settings = await chrome.storage.local.get(['jiraDomain', 'jiraEmail', 'jiraToken']);
// Create form data with the video
const formData = new FormData();
formData.append('file', videoBlob, 'screen-recording.webm');
// Upload the attachment
const response = await fetch(`https://${settings.jiraDomain}/rest/api/2/issue/${issueKey}/attachments`, {
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`${settings.jiraEmail}:${settings.jiraToken}`)}`,
'X-Atlassian-Token': 'no-check' // Required for file uploads
},
body: formData
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to attach video: ${error}`);
}
console.log('Video attached successfully');
} catch (error) {
console.error('Error attaching video:', error);
throw new Error('Failed to attach video: ' + error.message);
}
}
async captureScreenshot() {
try {
// Get current tab
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
});
console.log('Current tab:', tab);
// Capture the visible tab
console.log('Capturing screenshot...');
const screenshot = await chrome.tabs.captureVisibleTab(null, {
format: 'png'
});
console.log('Screenshot captured:', (screenshot === null || screenshot === void 0 ? void 0 : screenshot.substring(0, 100)) + '...');
// Verify screenshot data
if (!screenshot || !screenshot.startsWith('data:image')) {
throw new Error('Invalid screenshot data');
}
// Store the screenshot data
this.screenshot = screenshot;
// Calculate window dimensions
const maxWidth = Math.min(1200, window.screen.availWidth * 0.8);
const maxHeight = Math.min(800, window.screen.availHeight * 0.8);
const left = (window.screen.availWidth - maxWidth) / 2;
const top = (window.screen.availHeight - maxHeight) / 2;
// Open editor window
console.log('Opening editor window...');
const editorUrl = chrome.runtime.getURL('editor.html');
this.editorWindow = window.open(editorUrl, 'screenshot-editor', `width=${maxWidth},height=${maxHeight},left=${left},top=${top},resizable=yes`);
if (!this.editorWindow) {
throw new Error('Could not open editor window. Please allow popups for this extension.');
}
// Send screenshot to background script
chrome.runtime.sendMessage({
type: 'init-editor',
screenshot: this.screenshot
}, response => {
console.log('Screenshot sent to background, response:', response);
});
} catch (error) {
console.error('Screenshot error:', error);
this.showNotification('Error capturing screenshot: ' + error.message, 'error');
}
}
async startScreenRecording() {
try {
// Request screen capture
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: "always",
frameRate: {
ideal: 30
}
},
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100
}
});
// Create MediaRecorder
this.mediaRecorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9',
videoBitsPerSecond: 3000000 // 3 Mbps
});
// Set up recording handlers
this.recordedChunks = [];
this.mediaRecorder.ondataavailable = event => {
if (event.data.size > 0) {
this.recordedChunks.push(event.data);
}
};
// Handle recording stop
this.mediaRecorder.onstop = async () => {
// Hide recording dialog
this.recordingDialog.classList.add('hidden');
// Stop all tracks
stream.getTracks().forEach(track => track.stop());
// Create blob from recorded chunks
const blob = new Blob(this.recordedChunks, {
type: 'video/webm'
});
// Convert to MP4 using FFmpeg.js (to be implemented)
try {
const mp4Blob = await this.convertToMP4(blob);
// Create object URL for preview
const videoURL = URL.createObjectURL(mp4Blob);
// Add video preview to attachment preview
this.attachmentPreview.innerHTML = `
<div class="video-preview">
<video src="${videoURL}" controls style="max-width: 100%; height: auto;"></video>
</div>
`;
// Store the video blob for later submission
this.recordedVideo = mp4Blob;
// Show the main form again and restore window size
document.body.classList.remove('recording');
this.feedbackForm.classList.remove('hidden');
window.resizeTo(400, 600); // Restore original size
this.showNotification('Recording saved successfully!', 'success');
} catch (error) {
console.error('Error converting video:', error);
this.showNotification('Error converting video: ' + error.message, 'error');
}
};
// Start recording
this.mediaRecorder.start(1000); // Collect data every second
// Hide the main form and show recording dialog
document.body.classList.add('recording');
this.feedbackForm.classList.add('hidden');
this.recordingDialog.classList.remove('hidden');
// Set up stop recording button
const stopButton = document.getElementById('stop-recording');
stopButton.onclick = () => {
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
this.mediaRecorder.stop();
}
};
// Minimize window to only show recording controls
window.resizeTo(300, 100);
} catch (error) {
console.error('Screen recording error:', error);
this.showNotification('Error starting screen recording: ' + error.message, 'error');
// Make sure main form is visible if there's an error
document.body.classList.remove('recording');
this.feedbackForm.classList.remove('hidden');
}
}
async convertToMP4(webmBlob) {
// For now, we'll just return the webm blob
// In a future update, we can implement proper conversion to MP4
return webmBlob;
}
showSettings() {
this.settingsPanel.classList.remove('hidden');
this.feedbackForm.classList.add('hidden');
}
showFeedbackForm() {
this.settingsPanel.classList.add('hidden');
this.feedbackForm.classList.remove('hidden');
}
showNotification(message, type = 'info') {
this.notification.textContent = message;
this.notification.className = `notification ${type}`;
this.notification.classList.remove('hidden');
setTimeout(() => {
this.notification.classList.add('hidden');
}, 5000);
}
}
// Initialize the extension
new FeedbackExtension();
})();
/******/ })()
;