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">
|
<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;
|
||||||
|
140
dist/popup.js
vendored
140
dist/popup.js
vendored
@ -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,16 +683,22 @@ ${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) {
|
||||||
|
if (this.screenshot) {
|
||||||
await this.attachScreenshotToIssue(response.key, 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');
|
||||||
document.getElementById('feedback-title').value = '';
|
document.getElementById('feedback-title').value = '';
|
||||||
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) {
|
} 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');
|
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
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;
|
||||||
|
}
|
144
js/popup.js
144
js/popup.js
@ -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,10 +232,15 @@ ${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) {
|
||||||
|
if (this.screenshot) {
|
||||||
await this.attachScreenshotToIssue(response.key, 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');
|
||||||
@ -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) {
|
} 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');
|
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');
|
||||||
|
@ -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;
|
||||||
|
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