/******/ (() => { // 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.shapes = []; this.selectedShape = null; this.isDragging = false; this.originalImage = null; this.currentState = null; this.scale = 1; this.paths = []; this.currentPath = []; 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 = ['select', 'move', '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 DPR and canvas scaling this.startX = (e.clientX - rect.left) * (this.canvas.width / (rect.width * dpr)); this.startY = (e.clientY - rect.top) * (this.canvas.height / (rect.height * dpr)); this.lastX = this.startX; this.lastY = this.startY; if (this.drawingMode === 'pen') { // Start a new path array for the current drawing this.currentPath = []; this.currentPath.push({ x: this.startX, y: this.startY }); 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) * (this.canvas.width / (rect.width * dpr)); const currentY = (e.clientY - rect.top) * (this.canvas.height / (rect.height * dpr)); // Get the current state as an image const baseImage = new Image(); baseImage.src = this.currentState || this.canvas.toDataURL(); // 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); } 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': // Add point to current path this.currentPath.push({ x: currentX, y: currentY }); // Draw the entire current path 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; } } 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) { this.isDrawing = false; // Save the current state with the completed drawing 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(); img.onload = () => { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(img, 0, 0); }; 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; } // 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); } else if (this.originalImage) { this.ctx.drawImage(this.originalImage, 0, 0, this.canvas.width, this.canvas.height); } } } 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; // 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 = `
Screenshot
`; // 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'); } 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, attach it to the created issue if (this.screenshot && response.key) { await this.attachScreenshotToIssue(response.key, this.screenshot); } 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; } } 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 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 { const stream = await navigator.mediaDevices.getDisplayMedia({ video: true }); // Recording implementation here this.showNotification('Screen recording feature coming soon!', 'info'); } catch (error) { this.showNotification('Error starting screen recording: ' + error.message, 'error'); } } 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(); })(); /******/ })() ;