Video-feature

This commit is contained in:
Bhav Kushwaha 2025-01-26 15:57:06 +05:30
parent 9aed623f04
commit 53cdd26f48
6 changed files with 436 additions and 12 deletions

1
dist/popup.html vendored
View File

@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Jira Feedback</title> <title>Jira Feedback</title>
<link rel="stylesheet" href="styles/popup.css"> <link rel="stylesheet" href="styles/popup.css">
<link rel="stylesheet" href="styles/recording-dialog.css">
<style> <style>
.notification { .notification {
position: fixed; position: fixed;

142
dist/popup.js vendored
View File

@ -458,6 +458,10 @@ class FeedbackExtension {
constructor() { constructor() {
this.screenshot = null; this.screenshot = null;
this.editorWindow = null; this.editorWindow = null;
this.mediaRecorder = null;
this.recordedChunks = [];
this.recordingDialog = null;
this.recordedVideo = null;
// Initialize when DOM is loaded // Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@ -527,6 +531,17 @@ class FeedbackExtension {
// Notification // Notification
this.notification = document.getElementById('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() { attachEventListeners() {
if (this.screenshotBtn) { if (this.screenshotBtn) {
@ -668,9 +683,14 @@ ${description}
console.log('Submitting to Jira with data:', JSON.stringify(formData, null, 2)); console.log('Submitting to Jira with data:', JSON.stringify(formData, null, 2));
const response = await this.submitToJira(formData); const response = await this.submitToJira(formData);
// If we have a screenshot, attach it to the created issue // If we have a screenshot or video, attach it to the created issue
if (this.screenshot && response.key) { if (response.key) {
await this.attachScreenshotToIssue(response.key, this.screenshot); if (this.screenshot) {
await this.attachScreenshotToIssue(response.key, this.screenshot);
}
if (this.recordedVideo) {
await this.attachVideoToIssue(response.key, this.recordedVideo);
}
} }
if (response.key) { if (response.key) {
this.showNotification(`Issue ${response.key} created successfully!`, 'success'); this.showNotification(`Issue ${response.key} created successfully!`, 'success');
@ -678,6 +698,7 @@ ${description}
document.getElementById('feedback-description').value = ''; document.getElementById('feedback-description').value = '';
this.attachmentPreview.innerHTML = ''; this.attachmentPreview.innerHTML = '';
this.screenshot = null; this.screenshot = null;
this.recordedVideo = null;
} }
} catch (error) { } catch (error) {
console.error('Full error details:', error); console.error('Full error details:', error);
@ -788,6 +809,33 @@ ${description}
throw new Error('Failed to attach screenshot: ' + error.message); 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() { async captureScreenshot() {
try { try {
// Get current tab // Get current tab
@ -840,15 +888,97 @@ ${description}
} }
async startScreenRecording() { async startScreenRecording() {
try { try {
// Request screen capture
const stream = await navigator.mediaDevices.getDisplayMedia({ const stream = await navigator.mediaDevices.getDisplayMedia({
video: true video: {
cursor: "always",
frameRate: {
ideal: 30
}
},
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100
}
}); });
// Recording implementation here
this.showNotification('Screen recording feature coming soon!', 'info'); // 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;
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
// Show recording dialog
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 extension popup
window.resizeTo(300, 100);
} catch (error) { } catch (error) {
console.error('Screen recording error:', error);
this.showNotification('Error starting screen recording: ' + error.message, 'error'); this.showNotification('Error starting screen recording: ' + error.message, 'error');
} }
} }
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() { showSettings() {
this.settingsPanel.classList.remove('hidden'); this.settingsPanel.classList.remove('hidden');
this.feedbackForm.classList.add('hidden'); this.feedbackForm.classList.add('hidden');

79
dist/styles/recording-dialog.css vendored Normal file
View File

@ -0,0 +1,79 @@
.recording-dialog {
position: fixed;
top: 10px;
right: 10px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 15px;
z-index: 10000;
width: 250px;
}
.recording-dialog.hidden {
display: none;
}
.recording-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.recording-status {
display: flex;
align-items: center;
gap: 8px;
color: #DE350B;
font-weight: 500;
}
.recording-status::before {
content: '';
display: inline-block;
width: 10px;
height: 10px;
background: #DE350B;
border-radius: 50%;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
#stop-recording {
background: #DE350B;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
}
#stop-recording:hover {
background: #BF2600;
}
.video-preview {
margin-top: 10px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
}
.video-preview video {
width: 100%;
border-radius: 4px;
}

View File

@ -4,6 +4,10 @@ class FeedbackExtension {
constructor() { constructor() {
this.screenshot = null; this.screenshot = null;
this.editorWindow = null; this.editorWindow = null;
this.mediaRecorder = null;
this.recordedChunks = [];
this.recordingDialog = null;
this.recordedVideo = null;
// Initialize when DOM is loaded // Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@ -67,6 +71,17 @@ class FeedbackExtension {
// Notification // Notification
this.notification = document.getElementById('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() { attachEventListeners() {
@ -217,9 +232,14 @@ ${description}
console.log('Submitting to Jira with data:', JSON.stringify(formData, null, 2)); console.log('Submitting to Jira with data:', JSON.stringify(formData, null, 2));
const response = await this.submitToJira(formData); const response = await this.submitToJira(formData);
// If we have a screenshot, attach it to the created issue // If we have a screenshot or video, attach it to the created issue
if (this.screenshot && response.key) { if (response.key) {
await this.attachScreenshotToIssue(response.key, this.screenshot); if (this.screenshot) {
await this.attachScreenshotToIssue(response.key, this.screenshot);
}
if (this.recordedVideo) {
await this.attachVideoToIssue(response.key, this.recordedVideo);
}
} }
if (response.key) { if (response.key) {
@ -228,6 +248,7 @@ ${description}
document.getElementById('feedback-description').value = ''; document.getElementById('feedback-description').value = '';
this.attachmentPreview.innerHTML = ''; this.attachmentPreview.innerHTML = '';
this.screenshot = null; this.screenshot = null;
this.recordedVideo = null;
} }
} catch (error) { } catch (error) {
console.error('Full error details:', error); console.error('Full error details:', error);
@ -352,6 +373,36 @@ ${description}
} }
} }
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() { async captureScreenshot() {
try { try {
// Get current tab // Get current tab
@ -408,14 +459,97 @@ ${description}
async startScreenRecording() { async startScreenRecording() {
try { try {
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true }); // Request screen capture
// Recording implementation here const stream = await navigator.mediaDevices.getDisplayMedia({
this.showNotification('Screen recording feature coming soon!', 'info'); 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;
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
// Show recording dialog
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 extension popup
window.resizeTo(300, 100);
} catch (error) { } catch (error) {
console.error('Screen recording error:', error);
this.showNotification('Error starting screen recording: ' + error.message, 'error'); this.showNotification('Error starting screen recording: ' + error.message, 'error');
} }
} }
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() { showSettings() {
this.settingsPanel.classList.remove('hidden'); this.settingsPanel.classList.remove('hidden');
this.feedbackForm.classList.add('hidden'); this.feedbackForm.classList.add('hidden');

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Jira Feedback</title> <title>Jira Feedback</title>
<link rel="stylesheet" href="styles/popup.css"> <link rel="stylesheet" href="styles/popup.css">
<link rel="stylesheet" href="styles/recording-dialog.css">
<style> <style>
.notification { .notification {
position: fixed; position: fixed;

View File

@ -0,0 +1,79 @@
.recording-dialog {
position: fixed;
top: 10px;
right: 10px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 15px;
z-index: 10000;
width: 250px;
}
.recording-dialog.hidden {
display: none;
}
.recording-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.recording-status {
display: flex;
align-items: center;
gap: 8px;
color: #DE350B;
font-weight: 500;
}
.recording-status::before {
content: '';
display: inline-block;
width: 10px;
height: 10px;
background: #DE350B;
border-radius: 50%;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
#stop-recording {
background: #DE350B;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
}
#stop-recording:hover {
background: #BF2600;
}
.video-preview {
margin-top: 10px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
}
.video-preview video {
width: 100%;
border-radius: 4px;
}