Added live captions for a browser source
This commit is contained in:
parent
f97d3a44f9
commit
1cc30b80ad
32
app.py
32
app.py
@ -161,6 +161,38 @@ def handle_audio_data(data):
|
||||
emit('error', {'message': 'Failed to process audio'})
|
||||
|
||||
|
||||
@socketio.on('desktop_audio_data')
|
||||
def handle_desktop_audio_data(data):
|
||||
"""
|
||||
Handle incoming desktop audio data from client.
|
||||
|
||||
Args:
|
||||
data: Dictionary containing 'audio' (base64 or bytes) and 'format'
|
||||
"""
|
||||
try:
|
||||
audio_bytes = data.get('audio')
|
||||
audio_format = data.get('format', 'webm')
|
||||
|
||||
if not audio_bytes:
|
||||
return
|
||||
|
||||
# Handle base64 encoded audio
|
||||
if isinstance(audio_bytes, str):
|
||||
import base64
|
||||
audio_bytes = base64.b64decode(audio_bytes)
|
||||
|
||||
# Transcribe audio
|
||||
text = transcriber.transcribe_audio(audio_bytes, format=audio_format)
|
||||
|
||||
if text:
|
||||
logger.info(f"Desktop transcription: {text}")
|
||||
emit('desktop_transcription', {'text': text})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing desktop audio: {e}")
|
||||
emit('error', {'message': 'Failed to process desktop audio'})
|
||||
|
||||
|
||||
@socketio.on('save_recording')
|
||||
def handle_save_recording(data):
|
||||
"""Handle saving a recording session."""
|
||||
|
||||
40
database.py
40
database.py
@ -8,6 +8,7 @@ from datetime import datetime
|
||||
|
||||
# Default settings
|
||||
DEFAULT_SETTINGS = {
|
||||
# Microphone caption settings
|
||||
'font_family': 'Arial, sans-serif',
|
||||
'font_size': 32,
|
||||
'font_weight': 'normal',
|
||||
@ -18,6 +19,17 @@ DEFAULT_SETTINGS = {
|
||||
'text_align': 'center',
|
||||
'padding': 20,
|
||||
'border_radius': 10,
|
||||
# Desktop audio caption settings
|
||||
'desktop_font_family': 'Arial, sans-serif',
|
||||
'desktop_font_size': 28,
|
||||
'desktop_font_weight': 'normal',
|
||||
'desktop_text_color': '#90EE90',
|
||||
'desktop_background_color': '#1a2e1a',
|
||||
'desktop_background_opacity': 0.9,
|
||||
'desktop_max_words': 30,
|
||||
'desktop_text_align': 'center',
|
||||
'desktop_padding': 20,
|
||||
'desktop_border_radius': 10,
|
||||
}
|
||||
|
||||
|
||||
@ -57,6 +69,24 @@ def init_db():
|
||||
cursor.execute('ALTER TABLE user_settings ADD COLUMN max_words INTEGER DEFAULT 30')
|
||||
conn.commit()
|
||||
|
||||
# Add desktop audio columns if they don't exist
|
||||
if 'desktop_font_family' not in columns:
|
||||
desktop_columns = [
|
||||
('desktop_font_family', 'TEXT', "'Arial, sans-serif'"),
|
||||
('desktop_font_size', 'INTEGER', '28'),
|
||||
('desktop_font_weight', 'TEXT', "'normal'"),
|
||||
('desktop_text_color', 'TEXT', "'#90EE90'"),
|
||||
('desktop_background_color', 'TEXT', "'#1a2e1a'"),
|
||||
('desktop_background_opacity', 'REAL', '0.9'),
|
||||
('desktop_max_words', 'INTEGER', '30'),
|
||||
('desktop_text_align', 'TEXT', "'center'"),
|
||||
('desktop_padding', 'INTEGER', '20'),
|
||||
('desktop_border_radius', 'INTEGER', '10'),
|
||||
]
|
||||
for col_name, col_type, default in desktop_columns:
|
||||
cursor.execute(f'ALTER TABLE user_settings ADD COLUMN {col_name} {col_type} DEFAULT {default}')
|
||||
conn.commit()
|
||||
|
||||
# Remove old columns that are no longer needed (fade_delay, max_lines)
|
||||
# SQLite doesn't support DROP COLUMN easily, so we just ignore old columns
|
||||
else:
|
||||
@ -74,6 +104,16 @@ def init_db():
|
||||
text_align TEXT DEFAULT 'center',
|
||||
padding INTEGER DEFAULT 20,
|
||||
border_radius INTEGER DEFAULT 10,
|
||||
desktop_font_family TEXT DEFAULT 'Arial, sans-serif',
|
||||
desktop_font_size INTEGER DEFAULT 28,
|
||||
desktop_font_weight TEXT DEFAULT 'normal',
|
||||
desktop_text_color TEXT DEFAULT '#90EE90',
|
||||
desktop_background_color TEXT DEFAULT '#1a2e1a',
|
||||
desktop_background_opacity REAL DEFAULT 0.9,
|
||||
desktop_max_words INTEGER DEFAULT 30,
|
||||
desktop_text_align TEXT DEFAULT 'center',
|
||||
desktop_padding INTEGER DEFAULT 20,
|
||||
desktop_border_radius INTEGER DEFAULT 10,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
|
||||
@ -39,28 +39,71 @@ html, body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Caption Container */
|
||||
#caption-container {
|
||||
/* Captions Wrapper */
|
||||
#captions-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Caption Area (shared by mic and desktop) */
|
||||
.caption-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background-color: rgba(26, 26, 46, 0.9);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
font-size: 32px;
|
||||
font-family: Arial, sans-serif;
|
||||
text-align: center;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
#captions {
|
||||
.caption-area.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Caption Label with Status */
|
||||
.caption-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.caption-label .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
/* Caption Text */
|
||||
#captions,
|
||||
#desktop-captions {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Desktop Caption Container Default Styling */
|
||||
#desktop-caption-container {
|
||||
background-color: rgba(26, 46, 26, 0.9);
|
||||
color: #90EE90;
|
||||
}
|
||||
|
||||
/* Controls Bar */
|
||||
#controls {
|
||||
display: flex;
|
||||
@ -131,6 +174,16 @@ html, body {
|
||||
background-color: #5aff9a;
|
||||
}
|
||||
|
||||
/* Desktop Toggle Button Active State */
|
||||
#btn-desktop-toggle.active {
|
||||
background-color: var(--success);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#btn-desktop-toggle.active:hover:not(:disabled) {
|
||||
background-color: #5aff9a;
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.toggle-switch {
|
||||
display: flex;
|
||||
@ -296,6 +349,52 @@ html, body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Settings Sections (Collapsible) */
|
||||
.settings-section {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.section-toggle {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 15px;
|
||||
background-color: var(--bg-tertiary);
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition);
|
||||
}
|
||||
|
||||
.section-toggle:hover {
|
||||
background-color: #323258;
|
||||
}
|
||||
|
||||
.section-toggle.active {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 10px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 15px;
|
||||
background-color: var(--bg-tertiary);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.section-content.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Settings Groups */
|
||||
.setting-group {
|
||||
margin-bottom: 25px;
|
||||
|
||||
321
static/js/app.js
321
static/js/app.js
@ -7,21 +7,34 @@ const App = {
|
||||
// WebSocket connection
|
||||
socket: null,
|
||||
|
||||
// Audio recording
|
||||
// Mic audio recording
|
||||
mediaRecorder: null,
|
||||
audioStream: null,
|
||||
audioChunks: [],
|
||||
isRecording: false,
|
||||
recordingInterval: null,
|
||||
|
||||
// Continuous caption stream
|
||||
// Mic caption stream
|
||||
wordBuffer: [],
|
||||
pendingWords: [],
|
||||
wordAnimationTimer: null,
|
||||
|
||||
// Browser tab audio recording
|
||||
desktopMediaRecorder: null,
|
||||
desktopStream: null,
|
||||
desktopAudioChunks: [],
|
||||
isDesktopCapturing: false,
|
||||
desktopRecordingInterval: null,
|
||||
|
||||
// Browser tab caption stream
|
||||
desktopWordBuffer: [],
|
||||
desktopPendingWords: [],
|
||||
desktopWordAnimationTimer: null,
|
||||
|
||||
// Auto-save recording state
|
||||
sessionStartTime: null,
|
||||
sessionTranscript: [],
|
||||
desktopSessionTranscript: [],
|
||||
|
||||
// DOM elements
|
||||
elements: {},
|
||||
@ -43,13 +56,20 @@ const App = {
|
||||
*/
|
||||
cacheElements() {
|
||||
this.elements = {
|
||||
// Mic controls
|
||||
btnStart: document.getElementById('btn-start'),
|
||||
btnStop: document.getElementById('btn-stop'),
|
||||
btnClear: document.getElementById('btn-clear'),
|
||||
autoSaveToggle: document.getElementById('auto-save-toggle'),
|
||||
captions: document.getElementById('captions'),
|
||||
statusDot: document.getElementById('status-dot'),
|
||||
statusText: document.getElementById('status-text'),
|
||||
micStatusDot: document.getElementById('mic-status-dot'),
|
||||
micStatusText: document.getElementById('mic-status-text'),
|
||||
// Desktop controls
|
||||
btnDesktopToggle: document.getElementById('btn-desktop-toggle'),
|
||||
desktopCaptions: document.getElementById('desktop-captions'),
|
||||
desktopCaptionContainer: document.getElementById('desktop-caption-container'),
|
||||
desktopStatusDot: document.getElementById('desktop-status-dot'),
|
||||
desktopStatusText: document.getElementById('desktop-status-text'),
|
||||
};
|
||||
},
|
||||
|
||||
@ -57,10 +77,14 @@ const App = {
|
||||
* Bind event listeners
|
||||
*/
|
||||
bindEvents() {
|
||||
// Mic controls
|
||||
this.elements.btnStart.addEventListener('click', () => this.startRecording());
|
||||
this.elements.btnStop.addEventListener('click', () => this.stopRecording());
|
||||
this.elements.btnClear.addEventListener('click', () => this.clearCaptions());
|
||||
|
||||
// Desktop toggle
|
||||
this.elements.btnDesktopToggle.addEventListener('click', () => this.toggleDesktopCapture());
|
||||
|
||||
// Load auto-save preference from localStorage
|
||||
const savedPref = localStorage.getItem('autoSaveEnabled');
|
||||
if (savedPref === 'true') {
|
||||
@ -81,18 +105,22 @@ const App = {
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log('Connected to server');
|
||||
this.setStatus('connected', 'Connected');
|
||||
this.setMicStatus('connected', 'Ready');
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
console.log('Disconnected from server');
|
||||
this.setStatus('disconnected', 'Disconnected');
|
||||
this.setMicStatus('disconnected', 'Disconnected');
|
||||
});
|
||||
|
||||
this.socket.on('transcription', (data) => {
|
||||
this.addWords(data.text);
|
||||
});
|
||||
|
||||
this.socket.on('desktop_transcription', (data) => {
|
||||
this.addDesktopWords(data.text);
|
||||
});
|
||||
|
||||
this.socket.on('settings_updated', (settings) => {
|
||||
Settings.applySettings(settings);
|
||||
});
|
||||
@ -111,15 +139,23 @@ const App = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Update status indicator
|
||||
* Update mic status indicator
|
||||
*/
|
||||
setStatus(state, text) {
|
||||
this.elements.statusDot.className = `dot ${state}`;
|
||||
this.elements.statusText.textContent = text;
|
||||
setMicStatus(state, text) {
|
||||
this.elements.micStatusDot.className = `dot ${state}`;
|
||||
this.elements.micStatusText.textContent = text;
|
||||
},
|
||||
|
||||
/**
|
||||
* Start audio recording
|
||||
* Update desktop status indicator
|
||||
*/
|
||||
setDesktopStatus(state, text) {
|
||||
this.elements.desktopStatusDot.className = `dot ${state}`;
|
||||
this.elements.desktopStatusText.textContent = text;
|
||||
},
|
||||
|
||||
/**
|
||||
* Start mic audio recording
|
||||
*/
|
||||
async startRecording() {
|
||||
try {
|
||||
@ -134,7 +170,7 @@ const App = {
|
||||
this.isRecording = true;
|
||||
this.elements.btnStart.disabled = true;
|
||||
this.elements.btnStop.disabled = false;
|
||||
this.setStatus('recording', 'Recording...');
|
||||
this.setMicStatus('recording', 'Recording...');
|
||||
|
||||
// Reset session transcript for auto-save
|
||||
this.sessionStartTime = new Date();
|
||||
@ -145,7 +181,7 @@ const App = {
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error starting recording:', error);
|
||||
this.setStatus('error', 'Microphone access denied');
|
||||
this.setMicStatus('error', 'Microphone access denied');
|
||||
}
|
||||
},
|
||||
|
||||
@ -197,7 +233,7 @@ const App = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop audio recording
|
||||
* Stop mic audio recording
|
||||
*/
|
||||
stopRecording() {
|
||||
this.isRecording = false;
|
||||
@ -221,7 +257,7 @@ const App = {
|
||||
|
||||
this.elements.btnStart.disabled = false;
|
||||
this.elements.btnStop.disabled = true;
|
||||
this.setStatus('connected', 'Connected');
|
||||
this.setMicStatus('connected', 'Ready');
|
||||
|
||||
// Auto-save if enabled and we have content
|
||||
if (this.elements.autoSaveToggle.checked && this.sessionTranscript.length > 0) {
|
||||
@ -314,10 +350,10 @@ const App = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all captions
|
||||
* Clear all captions (both mic and desktop)
|
||||
*/
|
||||
clearCaptions() {
|
||||
// Clear animation timer
|
||||
// Clear mic animation timer
|
||||
if (this.wordAnimationTimer) {
|
||||
clearTimeout(this.wordAnimationTimer);
|
||||
this.wordAnimationTimer = null;
|
||||
@ -325,6 +361,257 @@ const App = {
|
||||
this.wordBuffer = [];
|
||||
this.pendingWords = [];
|
||||
this.elements.captions.textContent = '';
|
||||
|
||||
// Clear browser tab animation timer
|
||||
if (this.desktopWordAnimationTimer) {
|
||||
clearTimeout(this.desktopWordAnimationTimer);
|
||||
this.desktopWordAnimationTimer = null;
|
||||
}
|
||||
this.desktopWordBuffer = [];
|
||||
this.desktopPendingWords = [];
|
||||
this.elements.desktopCaptions.textContent = '';
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// Browser Tab Audio Capture
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Toggle browser tab audio capture on/off
|
||||
*/
|
||||
async toggleDesktopCapture() {
|
||||
if (this.isDesktopCapturing) {
|
||||
this.stopDesktopCapture();
|
||||
} else {
|
||||
await this.startDesktopCapture();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Start browser tab audio capture via getDisplayMedia
|
||||
*/
|
||||
async startDesktopCapture() {
|
||||
// Check browser support
|
||||
if (!navigator.mediaDevices.getDisplayMedia) {
|
||||
alert('Browser tab audio capture is not supported in this browser.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Request browser tab capture with audio (preferCurrentTab helps default to tabs)
|
||||
this.desktopStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: {
|
||||
displaySurface: 'browser', // Prefer browser tabs
|
||||
},
|
||||
audio: {
|
||||
echoCancellation: false,
|
||||
noiseSuppression: false,
|
||||
autoGainControl: false,
|
||||
},
|
||||
preferCurrentTab: false, // Don't default to current tab (we want to pick)
|
||||
selfBrowserSurface: 'exclude', // Exclude this tab from picker
|
||||
});
|
||||
|
||||
// Check if audio track was granted
|
||||
const audioTracks = this.desktopStream.getAudioTracks();
|
||||
if (audioTracks.length === 0) {
|
||||
alert('No audio track available. Please select a browser tab that has audio playing.');
|
||||
this.desktopStream.getTracks().forEach(track => track.stop());
|
||||
this.desktopStream = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop video track immediately (we only need audio)
|
||||
this.desktopStream.getVideoTracks().forEach(track => track.stop());
|
||||
|
||||
// Handle user stopping share via browser UI
|
||||
audioTracks[0].addEventListener('ended', () => {
|
||||
this.handleDesktopShareEnded();
|
||||
});
|
||||
|
||||
this.isDesktopCapturing = true;
|
||||
this.elements.btnDesktopToggle.classList.add('active');
|
||||
this.elements.btnDesktopToggle.innerHTML = '<span class="icon">🎧</span> Stop Tab';
|
||||
this.setDesktopStatus('recording', 'Capturing...');
|
||||
|
||||
// Show desktop caption container
|
||||
this.elements.desktopCaptionContainer.classList.remove('hidden');
|
||||
|
||||
// Reset desktop session transcript
|
||||
this.desktopSessionTranscript = [];
|
||||
|
||||
// Start recording cycle
|
||||
this.startDesktopRecordingCycle();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error starting desktop capture:', error);
|
||||
if (error.name === 'NotAllowedError') {
|
||||
// User cancelled the picker
|
||||
this.setDesktopStatus('', 'Cancelled');
|
||||
} else {
|
||||
this.setDesktopStatus('error', 'Failed to start');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop desktop audio capture
|
||||
*/
|
||||
stopDesktopCapture() {
|
||||
this.isDesktopCapturing = false;
|
||||
|
||||
// Clear recording interval
|
||||
if (this.desktopRecordingInterval) {
|
||||
clearTimeout(this.desktopRecordingInterval);
|
||||
this.desktopRecordingInterval = null;
|
||||
}
|
||||
|
||||
// Stop media recorder
|
||||
if (this.desktopMediaRecorder && this.desktopMediaRecorder.state === 'recording') {
|
||||
this.desktopMediaRecorder.stop();
|
||||
}
|
||||
|
||||
// Stop all tracks
|
||||
if (this.desktopStream) {
|
||||
this.desktopStream.getTracks().forEach(track => track.stop());
|
||||
this.desktopStream = null;
|
||||
}
|
||||
|
||||
this.elements.btnDesktopToggle.classList.remove('active');
|
||||
this.elements.btnDesktopToggle.innerHTML = '<span class="icon">🎧</span> Browser Tab';
|
||||
this.setDesktopStatus('', 'Stopped');
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle user clicking "Stop sharing" in browser UI
|
||||
*/
|
||||
handleDesktopShareEnded() {
|
||||
console.log('Browser tab share ended by user');
|
||||
this.stopDesktopCapture();
|
||||
},
|
||||
|
||||
/**
|
||||
* Start browser tab recording cycle - record for a duration, then send and restart
|
||||
*/
|
||||
startDesktopRecordingCycle() {
|
||||
if (!this.isDesktopCapturing || !this.desktopStream) return;
|
||||
|
||||
const audioTracks = this.desktopStream.getAudioTracks();
|
||||
if (audioTracks.length === 0) {
|
||||
this.stopDesktopCapture();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create audio-only stream from the desktop stream's audio track
|
||||
const audioOnlyStream = new MediaStream([audioTracks[0]]);
|
||||
|
||||
let mimeType = 'audio/webm';
|
||||
if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
|
||||
mimeType = 'audio/webm;codecs=opus';
|
||||
}
|
||||
|
||||
this.desktopAudioChunks = [];
|
||||
this.desktopMediaRecorder = new MediaRecorder(audioOnlyStream, { mimeType });
|
||||
|
||||
this.desktopMediaRecorder.ondataavailable = (event) => {
|
||||
if (event.data.size > 0) {
|
||||
this.desktopAudioChunks.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
this.desktopMediaRecorder.onstop = () => {
|
||||
if (this.desktopAudioChunks.length > 0) {
|
||||
const blob = new Blob(this.desktopAudioChunks, { type: 'audio/webm' });
|
||||
this.sendDesktopAudioBlob(blob);
|
||||
}
|
||||
|
||||
// Continue cycle if still capturing
|
||||
if (this.isDesktopCapturing) {
|
||||
this.startDesktopRecordingCycle();
|
||||
}
|
||||
};
|
||||
|
||||
this.desktopMediaRecorder.start();
|
||||
|
||||
// Same 1.5s chunk duration as mic
|
||||
const chunkDuration = 1500;
|
||||
this.desktopRecordingInterval = setTimeout(() => {
|
||||
if (this.desktopMediaRecorder && this.desktopMediaRecorder.state === 'recording') {
|
||||
this.desktopMediaRecorder.stop();
|
||||
}
|
||||
}, chunkDuration);
|
||||
},
|
||||
|
||||
/**
|
||||
* Send browser tab audio blob to server
|
||||
*/
|
||||
sendDesktopAudioBlob(blob) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const base64 = reader.result.split(',')[1];
|
||||
this.socket.emit('desktop_audio_data', {
|
||||
audio: base64,
|
||||
format: 'webm'
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add words to the browser tab caption stream
|
||||
*/
|
||||
addDesktopWords(text) {
|
||||
if (!text.trim()) return;
|
||||
|
||||
const newWords = text.trim().split(/\s+/);
|
||||
this.desktopPendingWords.push(...newWords);
|
||||
|
||||
// Track for auto-save if needed
|
||||
if (this.isDesktopCapturing) {
|
||||
this.desktopSessionTranscript.push(...newWords);
|
||||
}
|
||||
|
||||
if (!this.desktopWordAnimationTimer) {
|
||||
this.animateNextDesktopWord();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Animate browser tab words appearing one by one
|
||||
*/
|
||||
animateNextDesktopWord() {
|
||||
if (this.desktopPendingWords.length === 0) {
|
||||
this.desktopWordAnimationTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const word = this.desktopPendingWords.shift();
|
||||
this.desktopWordBuffer.push(word);
|
||||
|
||||
// Get max words from desktop settings
|
||||
const maxWords = Settings.current.desktop_max_words || 30;
|
||||
|
||||
while (this.desktopWordBuffer.length > maxWords) {
|
||||
this.desktopWordBuffer.shift();
|
||||
}
|
||||
|
||||
this.updateDesktopCaptionDisplay();
|
||||
|
||||
const baseDelay = 80;
|
||||
const minDelay = 30;
|
||||
const delay = this.desktopPendingWords.length > 10 ? minDelay : baseDelay;
|
||||
|
||||
this.desktopWordAnimationTimer = setTimeout(() => {
|
||||
this.animateNextDesktopWord();
|
||||
}, delay);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the browser tab caption display
|
||||
*/
|
||||
updateDesktopCaptionDisplay() {
|
||||
const text = this.desktopWordBuffer.join(' ');
|
||||
this.elements.desktopCaptions.textContent = text;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@ -30,7 +30,7 @@ const Settings = {
|
||||
btnSave: document.getElementById('btn-save-settings'),
|
||||
btnReset: document.getElementById('btn-reset-settings'),
|
||||
|
||||
// Text settings
|
||||
// Mic text settings
|
||||
fontFamily: document.getElementById('font-family'),
|
||||
fontSize: document.getElementById('font-size'),
|
||||
fontSizeValue: document.getElementById('font-size-value'),
|
||||
@ -38,7 +38,7 @@ const Settings = {
|
||||
textColor: document.getElementById('text-color'),
|
||||
textAlign: document.getElementById('text-align'),
|
||||
|
||||
// Background settings
|
||||
// Mic background settings
|
||||
backgroundColor: document.getElementById('background-color'),
|
||||
backgroundOpacity: document.getElementById('background-opacity'),
|
||||
opacityValue: document.getElementById('opacity-value'),
|
||||
@ -47,12 +47,37 @@ const Settings = {
|
||||
padding: document.getElementById('padding'),
|
||||
paddingValue: document.getElementById('padding-value'),
|
||||
|
||||
// Behavior settings
|
||||
// Mic behavior settings
|
||||
maxWords: document.getElementById('max-words'),
|
||||
maxWordsValue: document.getElementById('max-words-value'),
|
||||
|
||||
// Caption display
|
||||
// Desktop text settings
|
||||
desktopFontFamily: document.getElementById('desktop-font-family'),
|
||||
desktopFontSize: document.getElementById('desktop-font-size'),
|
||||
desktopFontSizeValue: document.getElementById('desktop-font-size-value'),
|
||||
desktopFontWeight: document.getElementById('desktop-font-weight'),
|
||||
desktopTextColor: document.getElementById('desktop-text-color'),
|
||||
desktopTextAlign: document.getElementById('desktop-text-align'),
|
||||
|
||||
// Desktop background settings
|
||||
desktopBackgroundColor: document.getElementById('desktop-background-color'),
|
||||
desktopBackgroundOpacity: document.getElementById('desktop-background-opacity'),
|
||||
desktopOpacityValue: document.getElementById('desktop-opacity-value'),
|
||||
desktopBorderRadius: document.getElementById('desktop-border-radius'),
|
||||
desktopRadiusValue: document.getElementById('desktop-radius-value'),
|
||||
desktopPadding: document.getElementById('desktop-padding'),
|
||||
desktopPaddingValue: document.getElementById('desktop-padding-value'),
|
||||
|
||||
// Desktop behavior settings
|
||||
desktopMaxWords: document.getElementById('desktop-max-words'),
|
||||
desktopMaxWordsValue: document.getElementById('desktop-max-words-value'),
|
||||
|
||||
// Caption displays
|
||||
captionContainer: document.getElementById('caption-container'),
|
||||
desktopCaptionContainer: document.getElementById('desktop-caption-container'),
|
||||
|
||||
// Section toggles
|
||||
sectionToggles: document.querySelectorAll('.section-toggle'),
|
||||
};
|
||||
},
|
||||
|
||||
@ -69,21 +94,40 @@ const Settings = {
|
||||
this.elements.btnSave.addEventListener('click', () => this.saveSettings());
|
||||
this.elements.btnReset.addEventListener('click', () => this.resetSettings());
|
||||
|
||||
// Live preview on input change
|
||||
const inputs = [
|
||||
// Section toggle collapse/expand
|
||||
this.elements.sectionToggles.forEach(toggle => {
|
||||
toggle.addEventListener('click', () => this.toggleSection(toggle));
|
||||
});
|
||||
|
||||
// Mic live preview on input change
|
||||
const micInputs = [
|
||||
'fontFamily', 'fontSize', 'fontWeight', 'textColor', 'textAlign',
|
||||
'backgroundColor', 'backgroundOpacity', 'borderRadius', 'padding',
|
||||
'maxWords'
|
||||
];
|
||||
|
||||
inputs.forEach(name => {
|
||||
micInputs.forEach(name => {
|
||||
const element = this.elements[name];
|
||||
if (element) {
|
||||
element.addEventListener('input', () => this.updatePreview());
|
||||
}
|
||||
});
|
||||
|
||||
// Update value displays for range inputs
|
||||
// Desktop live preview on input change
|
||||
const desktopInputs = [
|
||||
'desktopFontFamily', 'desktopFontSize', 'desktopFontWeight', 'desktopTextColor', 'desktopTextAlign',
|
||||
'desktopBackgroundColor', 'desktopBackgroundOpacity', 'desktopBorderRadius', 'desktopPadding',
|
||||
'desktopMaxWords'
|
||||
];
|
||||
|
||||
desktopInputs.forEach(name => {
|
||||
const element = this.elements[name];
|
||||
if (element) {
|
||||
element.addEventListener('input', () => this.updateDesktopPreview());
|
||||
}
|
||||
});
|
||||
|
||||
// Update mic value displays for range inputs
|
||||
this.elements.fontSize.addEventListener('input', (e) => {
|
||||
this.elements.fontSizeValue.textContent = e.target.value;
|
||||
});
|
||||
@ -99,6 +143,36 @@ const Settings = {
|
||||
this.elements.maxWords.addEventListener('input', (e) => {
|
||||
this.elements.maxWordsValue.textContent = e.target.value;
|
||||
});
|
||||
|
||||
// Update desktop value displays for range inputs
|
||||
this.elements.desktopFontSize.addEventListener('input', (e) => {
|
||||
this.elements.desktopFontSizeValue.textContent = e.target.value;
|
||||
});
|
||||
this.elements.desktopBackgroundOpacity.addEventListener('input', (e) => {
|
||||
this.elements.desktopOpacityValue.textContent = e.target.value;
|
||||
});
|
||||
this.elements.desktopBorderRadius.addEventListener('input', (e) => {
|
||||
this.elements.desktopRadiusValue.textContent = e.target.value;
|
||||
});
|
||||
this.elements.desktopPadding.addEventListener('input', (e) => {
|
||||
this.elements.desktopPaddingValue.textContent = e.target.value;
|
||||
});
|
||||
this.elements.desktopMaxWords.addEventListener('input', (e) => {
|
||||
this.elements.desktopMaxWordsValue.textContent = e.target.value;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle settings section collapse/expand
|
||||
*/
|
||||
toggleSection(toggle) {
|
||||
const sectionId = toggle.dataset.section;
|
||||
const section = document.getElementById(sectionId);
|
||||
const icon = toggle.querySelector('.toggle-icon');
|
||||
|
||||
section.classList.toggle('hidden');
|
||||
toggle.classList.toggle('active');
|
||||
icon.textContent = section.classList.contains('hidden') ? '\u25B6' : '\u25BC';
|
||||
},
|
||||
|
||||
/**
|
||||
@ -123,7 +197,7 @@ const Settings = {
|
||||
applySettings(settings) {
|
||||
this.current = settings;
|
||||
|
||||
// Update form values
|
||||
// Update mic form values
|
||||
this.elements.fontFamily.value = settings.font_family;
|
||||
this.elements.fontSize.value = settings.font_size;
|
||||
this.elements.fontSizeValue.textContent = settings.font_size;
|
||||
@ -142,12 +216,47 @@ const Settings = {
|
||||
this.elements.maxWords.value = settings.max_words || 30;
|
||||
this.elements.maxWordsValue.textContent = settings.max_words || 30;
|
||||
|
||||
// Apply to caption container
|
||||
// Apply to mic caption container
|
||||
this.updatePreview();
|
||||
|
||||
// Apply desktop settings if present
|
||||
if (settings.desktop_font_family !== undefined) {
|
||||
this.applyDesktopSettings(settings);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update live preview of caption styling
|
||||
* Apply desktop settings to the UI
|
||||
*/
|
||||
applyDesktopSettings(settings) {
|
||||
// Update desktop form values
|
||||
this.elements.desktopFontFamily.value = settings.desktop_font_family;
|
||||
this.elements.desktopFontSize.value = settings.desktop_font_size;
|
||||
this.elements.desktopFontSizeValue.textContent = settings.desktop_font_size;
|
||||
this.elements.desktopFontWeight.value = settings.desktop_font_weight;
|
||||
this.elements.desktopTextColor.value = settings.desktop_text_color;
|
||||
this.elements.desktopTextAlign.value = settings.desktop_text_align;
|
||||
|
||||
this.elements.desktopBackgroundColor.value = settings.desktop_background_color;
|
||||
this.elements.desktopBackgroundOpacity.value = Math.round(settings.desktop_background_opacity * 100);
|
||||
this.elements.desktopOpacityValue.textContent = Math.round(settings.desktop_background_opacity * 100);
|
||||
this.elements.desktopBorderRadius.value = settings.desktop_border_radius;
|
||||
this.elements.desktopRadiusValue.textContent = settings.desktop_border_radius;
|
||||
this.elements.desktopPadding.value = settings.desktop_padding;
|
||||
this.elements.desktopPaddingValue.textContent = settings.desktop_padding;
|
||||
|
||||
this.elements.desktopMaxWords.value = settings.desktop_max_words || 30;
|
||||
this.elements.desktopMaxWordsValue.textContent = settings.desktop_max_words || 30;
|
||||
|
||||
// Store desktop max words
|
||||
this.current.desktop_max_words = settings.desktop_max_words || 30;
|
||||
|
||||
// Apply to desktop caption container
|
||||
this.updateDesktopPreview();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update live preview of mic caption styling
|
||||
*/
|
||||
updatePreview() {
|
||||
const container = this.elements.captionContainer;
|
||||
@ -172,11 +281,40 @@ const Settings = {
|
||||
this.current.max_words = parseInt(this.elements.maxWords.value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update live preview of desktop caption styling
|
||||
*/
|
||||
updateDesktopPreview() {
|
||||
const container = this.elements.desktopCaptionContainer;
|
||||
if (!container) return;
|
||||
|
||||
const opacity = this.elements.desktopBackgroundOpacity.value / 100;
|
||||
|
||||
// Parse background color and apply opacity
|
||||
const bgColor = this.elements.desktopBackgroundColor.value;
|
||||
const r = parseInt(bgColor.slice(1, 3), 16);
|
||||
const g = parseInt(bgColor.slice(3, 5), 16);
|
||||
const b = parseInt(bgColor.slice(5, 7), 16);
|
||||
|
||||
container.style.fontFamily = this.elements.desktopFontFamily.value;
|
||||
container.style.fontSize = `${this.elements.desktopFontSize.value}px`;
|
||||
container.style.fontWeight = this.elements.desktopFontWeight.value;
|
||||
container.style.color = this.elements.desktopTextColor.value;
|
||||
container.style.textAlign = this.elements.desktopTextAlign.value;
|
||||
container.style.backgroundColor = `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
container.style.borderRadius = `${this.elements.desktopBorderRadius.value}px`;
|
||||
container.style.padding = `${this.elements.desktopPadding.value}px`;
|
||||
|
||||
// Store desktop max words for caption management
|
||||
this.current.desktop_max_words = parseInt(this.elements.desktopMaxWords.value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current form values as settings object
|
||||
*/
|
||||
getFormValues() {
|
||||
return {
|
||||
// Mic settings
|
||||
font_family: this.elements.fontFamily.value,
|
||||
font_size: parseInt(this.elements.fontSize.value),
|
||||
font_weight: this.elements.fontWeight.value,
|
||||
@ -187,6 +325,17 @@ const Settings = {
|
||||
border_radius: parseInt(this.elements.borderRadius.value),
|
||||
padding: parseInt(this.elements.padding.value),
|
||||
max_words: parseInt(this.elements.maxWords.value),
|
||||
// Desktop settings
|
||||
desktop_font_family: this.elements.desktopFontFamily.value,
|
||||
desktop_font_size: parseInt(this.elements.desktopFontSize.value),
|
||||
desktop_font_weight: this.elements.desktopFontWeight.value,
|
||||
desktop_text_color: this.elements.desktopTextColor.value,
|
||||
desktop_text_align: this.elements.desktopTextAlign.value,
|
||||
desktop_background_color: this.elements.desktopBackgroundColor.value,
|
||||
desktop_background_opacity: this.elements.desktopBackgroundOpacity.value / 100,
|
||||
desktop_border_radius: parseInt(this.elements.desktopBorderRadius.value),
|
||||
desktop_padding: parseInt(this.elements.desktopPadding.value),
|
||||
desktop_max_words: parseInt(this.elements.desktopMaxWords.value),
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@ -8,18 +8,37 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Caption Display Area -->
|
||||
<div id="caption-container">
|
||||
<div id="captions"></div>
|
||||
<!-- Caption Display Areas -->
|
||||
<div id="captions-wrapper">
|
||||
<!-- Microphone Caption Display -->
|
||||
<div id="caption-container" class="caption-area">
|
||||
<div class="caption-label">
|
||||
<span id="mic-status-dot" class="dot"></span>
|
||||
<span id="mic-status-text">Microphone</span>
|
||||
</div>
|
||||
<div id="captions"></div>
|
||||
</div>
|
||||
|
||||
<!-- Browser Tab Caption Display (hidden by default) -->
|
||||
<div id="desktop-caption-container" class="caption-area hidden">
|
||||
<div class="caption-label">
|
||||
<span id="desktop-status-dot" class="dot"></span>
|
||||
<span id="desktop-status-text">Browser Tab</span>
|
||||
</div>
|
||||
<div id="desktop-captions"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls Bar -->
|
||||
<div id="controls">
|
||||
<button id="btn-start" class="btn btn-primary">
|
||||
<span class="icon">►</span> Start
|
||||
<span class="icon">►</span> Start Mic
|
||||
</button>
|
||||
<button id="btn-stop" class="btn btn-danger" disabled>
|
||||
<span class="icon">■</span> Stop
|
||||
<span class="icon">■</span> Stop Mic
|
||||
</button>
|
||||
<button id="btn-desktop-toggle" class="btn btn-secondary" title="Capture audio from a browser tab">
|
||||
<span class="icon">🎧</span> Browser Tab
|
||||
</button>
|
||||
<button id="btn-clear" class="btn btn-secondary">
|
||||
Clear
|
||||
@ -37,12 +56,6 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Status Indicator -->
|
||||
<div id="status">
|
||||
<span id="status-dot" class="dot"></span>
|
||||
<span id="status-text">Ready</span>
|
||||
</div>
|
||||
|
||||
<!-- Settings Panel -->
|
||||
<div id="settings-panel" class="panel hidden">
|
||||
<div class="panel-header">
|
||||
@ -50,72 +63,139 @@
|
||||
<button id="btn-close-settings" class="btn-close">×</button>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<!-- Font Settings -->
|
||||
<div class="setting-group">
|
||||
<h3>Text</h3>
|
||||
<!-- Microphone Settings Section -->
|
||||
<div class="settings-section">
|
||||
<button class="section-toggle active" data-section="mic-settings">
|
||||
<span class="toggle-icon">▼</span> Microphone Captions
|
||||
</button>
|
||||
<div id="mic-settings" class="section-content">
|
||||
<div class="setting-group">
|
||||
<h3>Text</h3>
|
||||
<label for="font-family">Font Family</label>
|
||||
<select id="font-family">
|
||||
<option value="Arial, sans-serif">Arial</option>
|
||||
<option value="'Helvetica Neue', Helvetica, sans-serif">Helvetica</option>
|
||||
<option value="'Segoe UI', sans-serif">Segoe UI</option>
|
||||
<option value="'Roboto', sans-serif">Roboto</option>
|
||||
<option value="'Open Sans', sans-serif">Open Sans</option>
|
||||
<option value="Georgia, serif">Georgia</option>
|
||||
<option value="'Times New Roman', serif">Times New Roman</option>
|
||||
<option value="'Courier New', monospace">Courier New</option>
|
||||
<option value="monospace">Monospace</option>
|
||||
</select>
|
||||
|
||||
<label for="font-family">Font Family</label>
|
||||
<select id="font-family">
|
||||
<option value="Arial, sans-serif">Arial</option>
|
||||
<option value="'Helvetica Neue', Helvetica, sans-serif">Helvetica</option>
|
||||
<option value="'Segoe UI', sans-serif">Segoe UI</option>
|
||||
<option value="'Roboto', sans-serif">Roboto</option>
|
||||
<option value="'Open Sans', sans-serif">Open Sans</option>
|
||||
<option value="Georgia, serif">Georgia</option>
|
||||
<option value="'Times New Roman', serif">Times New Roman</option>
|
||||
<option value="'Courier New', monospace">Courier New</option>
|
||||
<option value="monospace">Monospace</option>
|
||||
</select>
|
||||
<label for="font-size">Font Size: <span id="font-size-value">32</span>px</label>
|
||||
<input type="range" id="font-size" min="16" max="72" value="32">
|
||||
|
||||
<label for="font-size">Font Size: <span id="font-size-value">32</span>px</label>
|
||||
<input type="range" id="font-size" min="16" max="72" value="32">
|
||||
<label for="font-weight">Font Weight</label>
|
||||
<select id="font-weight">
|
||||
<option value="normal">Normal</option>
|
||||
<option value="bold">Bold</option>
|
||||
<option value="lighter">Light</option>
|
||||
</select>
|
||||
|
||||
<label for="font-weight">Font Weight</label>
|
||||
<select id="font-weight">
|
||||
<option value="normal">Normal</option>
|
||||
<option value="bold">Bold</option>
|
||||
<option value="lighter">Light</option>
|
||||
</select>
|
||||
<label for="text-color">Text Color</label>
|
||||
<input type="color" id="text-color" value="#ffffff">
|
||||
|
||||
<label for="text-color">Text Color</label>
|
||||
<input type="color" id="text-color" value="#ffffff">
|
||||
<label for="text-align">Text Alignment</label>
|
||||
<select id="text-align">
|
||||
<option value="left">Left</option>
|
||||
<option value="center">Center</option>
|
||||
<option value="right">Right</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label for="text-align">Text Alignment</label>
|
||||
<select id="text-align">
|
||||
<option value="left">Left</option>
|
||||
<option value="center">Center</option>
|
||||
<option value="right">Right</option>
|
||||
</select>
|
||||
<div class="setting-group">
|
||||
<h3>Background</h3>
|
||||
<label for="background-color">Background Color</label>
|
||||
<input type="color" id="background-color" value="#1a1a2e">
|
||||
|
||||
<label for="background-opacity">Opacity: <span id="opacity-value">90</span>%</label>
|
||||
<input type="range" id="background-opacity" min="0" max="100" value="90">
|
||||
|
||||
<label for="border-radius">Corner Radius: <span id="radius-value">10</span>px</label>
|
||||
<input type="range" id="border-radius" min="0" max="30" value="10">
|
||||
|
||||
<label for="padding">Padding: <span id="padding-value">20</span>px</label>
|
||||
<input type="range" id="padding" min="5" max="50" value="20">
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<h3>Behavior</h3>
|
||||
<label for="max-words">Max Words: <span id="max-words-value">30</span></label>
|
||||
<input type="range" id="max-words" min="1" max="100" value="30">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Background Settings -->
|
||||
<div class="setting-group">
|
||||
<h3>Background</h3>
|
||||
<!-- Browser Tab Settings Section -->
|
||||
<div class="settings-section">
|
||||
<button class="section-toggle" data-section="desktop-settings">
|
||||
<span class="toggle-icon">▶</span> Browser Tab Captions
|
||||
</button>
|
||||
<div id="desktop-settings" class="section-content hidden">
|
||||
<div class="setting-group">
|
||||
<h3>Text</h3>
|
||||
<label for="desktop-font-family">Font Family</label>
|
||||
<select id="desktop-font-family">
|
||||
<option value="Arial, sans-serif">Arial</option>
|
||||
<option value="'Helvetica Neue', Helvetica, sans-serif">Helvetica</option>
|
||||
<option value="'Segoe UI', sans-serif">Segoe UI</option>
|
||||
<option value="'Roboto', sans-serif">Roboto</option>
|
||||
<option value="'Open Sans', sans-serif">Open Sans</option>
|
||||
<option value="Georgia, serif">Georgia</option>
|
||||
<option value="'Times New Roman', serif">Times New Roman</option>
|
||||
<option value="'Courier New', monospace">Courier New</option>
|
||||
<option value="monospace">Monospace</option>
|
||||
</select>
|
||||
|
||||
<label for="background-color">Background Color</label>
|
||||
<input type="color" id="background-color" value="#1a1a2e">
|
||||
<label for="desktop-font-size">Font Size: <span id="desktop-font-size-value">28</span>px</label>
|
||||
<input type="range" id="desktop-font-size" min="16" max="72" value="28">
|
||||
|
||||
<label for="background-opacity">Opacity: <span id="opacity-value">90</span>%</label>
|
||||
<input type="range" id="background-opacity" min="0" max="100" value="90">
|
||||
<label for="desktop-font-weight">Font Weight</label>
|
||||
<select id="desktop-font-weight">
|
||||
<option value="normal">Normal</option>
|
||||
<option value="bold">Bold</option>
|
||||
<option value="lighter">Light</option>
|
||||
</select>
|
||||
|
||||
<label for="border-radius">Corner Radius: <span id="radius-value">10</span>px</label>
|
||||
<input type="range" id="border-radius" min="0" max="30" value="10">
|
||||
<label for="desktop-text-color">Text Color</label>
|
||||
<input type="color" id="desktop-text-color" value="#90EE90">
|
||||
|
||||
<label for="padding">Padding: <span id="padding-value">20</span>px</label>
|
||||
<input type="range" id="padding" min="5" max="50" value="20">
|
||||
</div>
|
||||
<label for="desktop-text-align">Text Alignment</label>
|
||||
<select id="desktop-text-align">
|
||||
<option value="left">Left</option>
|
||||
<option value="center">Center</option>
|
||||
<option value="right">Right</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Caption Behavior -->
|
||||
<div class="setting-group">
|
||||
<h3>Behavior</h3>
|
||||
<div class="setting-group">
|
||||
<h3>Background</h3>
|
||||
<label for="desktop-background-color">Background Color</label>
|
||||
<input type="color" id="desktop-background-color" value="#1a2e1a">
|
||||
|
||||
<label for="max-words">Max Words: <span id="max-words-value">30</span></label>
|
||||
<input type="range" id="max-words" min="1" max="100" value="30">
|
||||
<label for="desktop-background-opacity">Opacity: <span id="desktop-opacity-value">90</span>%</label>
|
||||
<input type="range" id="desktop-background-opacity" min="0" max="100" value="90">
|
||||
|
||||
<label for="desktop-border-radius">Corner Radius: <span id="desktop-radius-value">10</span>px</label>
|
||||
<input type="range" id="desktop-border-radius" min="0" max="30" value="10">
|
||||
|
||||
<label for="desktop-padding">Padding: <span id="desktop-padding-value">20</span>px</label>
|
||||
<input type="range" id="desktop-padding" min="5" max="50" value="20">
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<h3>Behavior</h3>
|
||||
<label for="desktop-max-words">Max Words: <span id="desktop-max-words-value">30</span></label>
|
||||
<input type="range" id="desktop-max-words" min="1" max="100" value="30">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="setting-actions">
|
||||
<button id="btn-save-settings" class="btn btn-primary">Save Settings</button>
|
||||
<button id="btn-save-settings" class="btn btn-primary">Save All Settings</button>
|
||||
<button id="btn-reset-settings" class="btn btn-secondary">Reset to Defaults</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user