Video-feature
This commit is contained in:
parent
9aed623f04
commit
53cdd26f48
1
dist/popup.html
vendored
1
dist/popup.html
vendored
@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<title>Jira Feedback</title>
|
||||
<link rel="stylesheet" href="styles/popup.css">
|
||||
<link rel="stylesheet" href="styles/recording-dialog.css">
|
||||
<style>
|
||||
.notification {
|
||||
position: fixed;
|
||||
|
142
dist/popup.js
vendored
142
dist/popup.js
vendored
@ -458,6 +458,10 @@ 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', () => {
|
||||
@ -527,6 +531,17 @@ class FeedbackExtension {
|
||||
|
||||
// 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) {
|
||||
@ -668,9 +683,14 @@ ${description}
|
||||
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 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');
|
||||
@ -678,6 +698,7 @@ ${description}
|
||||
document.getElementById('feedback-description').value = '';
|
||||
this.attachmentPreview.innerHTML = '';
|
||||
this.screenshot = null;
|
||||
this.recordedVideo = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Full error details:', error);
|
||||
@ -788,6 +809,33 @@ ${description}
|
||||
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
|
||||
@ -840,15 +888,97 @@ ${description}
|
||||
}
|
||||
async startScreenRecording() {
|
||||
try {
|
||||
// Request screen capture
|
||||
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) {
|
||||
console.error('Screen recording error:', 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() {
|
||||
this.settingsPanel.classList.remove('hidden');
|
||||
this.feedbackForm.classList.add('hidden');
|
||||
|
79
dist/styles/recording-dialog.css
vendored
Normal file
79
dist/styles/recording-dialog.css
vendored
Normal 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;
|
||||
}
|
146
js/popup.js
146
js/popup.js
@ -4,6 +4,10 @@ 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', () => {
|
||||
@ -67,6 +71,17 @@ class FeedbackExtension {
|
||||
|
||||
// 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() {
|
||||
@ -217,9 +232,14 @@ ${description}
|
||||
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 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) {
|
||||
@ -228,6 +248,7 @@ ${description}
|
||||
document.getElementById('feedback-description').value = '';
|
||||
this.attachmentPreview.innerHTML = '';
|
||||
this.screenshot = null;
|
||||
this.recordedVideo = null;
|
||||
}
|
||||
} catch (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() {
|
||||
try {
|
||||
// Get current tab
|
||||
@ -408,14 +459,97 @@ ${description}
|
||||
|
||||
async startScreenRecording() {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
|
||||
// Recording implementation here
|
||||
this.showNotification('Screen recording feature coming soon!', 'info');
|
||||
// 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;
|
||||
|
||||
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) {
|
||||
console.error('Screen recording error:', 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() {
|
||||
this.settingsPanel.classList.remove('hidden');
|
||||
this.feedbackForm.classList.add('hidden');
|
||||
|
@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<title>Jira Feedback</title>
|
||||
<link rel="stylesheet" href="styles/popup.css">
|
||||
<link rel="stylesheet" href="styles/recording-dialog.css">
|
||||
<style>
|
||||
.notification {
|
||||
position: fixed;
|
||||
|
79
styles/recording-dialog.css
Normal file
79
styles/recording-dialog.css
Normal 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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user