2024-07-02 19:18:47 +00:00
|
|
|
import { Modal, setIcon } from 'obsidian';
|
|
|
|
|
|
|
|
|
|
export class SmartMemosAudioRecordModal extends Modal {
|
|
|
|
|
private mediaRecorder: MediaRecorder | null = null;
|
|
|
|
|
private chunks: BlobPart[] = [];
|
|
|
|
|
private resolve: (value: Blob | PromiseLike<Blob>) => void;
|
|
|
|
|
private reject: (reason?: any) => void;
|
2024-08-21 14:40:06 +00:00
|
|
|
private handleAudioRecording: (audioFile: Blob | null, transcribe: boolean, keepAudio: boolean, includeAudioFileLink: boolean) => void;
|
2024-07-02 19:18:47 +00:00
|
|
|
private isRecording: boolean = false;
|
|
|
|
|
private timer: HTMLElement;
|
|
|
|
|
private intervalId: number | null = null;
|
|
|
|
|
private startTime: number = 0;
|
|
|
|
|
private elapsedTime: number = 0; // To keep track of the elapsed time
|
|
|
|
|
private redDot: HTMLElement;
|
|
|
|
|
private isResetting: boolean = false; // Flag to track reset state
|
2024-08-09 17:20:58 +00:00
|
|
|
private keepAudioCheckbox: HTMLElement; // Add a property for the checkbox
|
2024-08-21 14:40:06 +00:00
|
|
|
private includeAudioFileLinkCheckbox: HTMLElement; // Add a property for the checkbox
|
2024-08-09 17:20:58 +00:00
|
|
|
private settings: any;
|
2024-07-02 19:18:47 +00:00
|
|
|
|
2024-08-21 14:40:06 +00:00
|
|
|
constructor(app: any, handleAudioRecording: (audioFile: Blob | null, transcribe: boolean, keepAudio: boolean, includeAudioFileLink: boolean) => void, settings: any) {
|
2024-07-02 19:18:47 +00:00
|
|
|
super(app);
|
|
|
|
|
this.handleAudioRecording = handleAudioRecording;
|
2024-08-09 17:20:58 +00:00
|
|
|
this.settings = settings; // Initialize settings
|
2024-07-02 19:18:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onOpen() {
|
|
|
|
|
const { contentEl, modalEl } = this;
|
|
|
|
|
|
|
|
|
|
if (!contentEl || !modalEl) {
|
|
|
|
|
console.error('contentEl or modalEl is null');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply initial recording state
|
|
|
|
|
modalEl.addClass('smart-memo-recording');
|
|
|
|
|
|
|
|
|
|
// Header and timer container
|
|
|
|
|
const headerTimerContainer = contentEl.createDiv({ cls: 'smart-memo-header-timer-container' });
|
|
|
|
|
const header = headerTimerContainer.createEl('h2', { text: 'Recording...', cls: 'smart-memo-recording-header' });
|
|
|
|
|
this.timer = headerTimerContainer.createEl('div', { cls: 'smart-memo-timer', text: '00:00' });
|
|
|
|
|
|
|
|
|
|
// Add specific class to modal-content
|
|
|
|
|
contentEl.addClass('smart-memo-audio-record-modal-content');
|
|
|
|
|
|
|
|
|
|
// Red dot animation container
|
|
|
|
|
const redDotContainer = contentEl.createDiv({ cls: 'smart-memo-red-dot-container' });
|
|
|
|
|
this.redDot = redDotContainer.createDiv({ cls: 'smart-memo-red-dot' });
|
|
|
|
|
|
|
|
|
|
// Control buttons group
|
|
|
|
|
const controlGroupWrapper = contentEl.createDiv({ cls: 'smart-memo-control-group-wrapper' });
|
|
|
|
|
const controlGroup = controlGroupWrapper.createDiv({ cls: 'smart-memo-modal-button-group' });
|
|
|
|
|
const playPauseButton = controlGroup.createEl('button', { cls: 'smart-memo-modal-button smart-memo-flex' });
|
|
|
|
|
const stopButton = controlGroup.createEl('button', { cls: 'smart-memo-modal-button smart-memo-flex' });
|
|
|
|
|
|
|
|
|
|
setIcon(playPauseButton, 'pause'); // Initially set to pause
|
|
|
|
|
setIcon(stopButton, 'square'); // Stop icon
|
|
|
|
|
|
|
|
|
|
stopButton.addEventListener('click', async () => {
|
|
|
|
|
const audioFile = await this.stopRecording();
|
2024-08-21 14:40:06 +00:00
|
|
|
this.handleAudioRecording(audioFile, false, (this.keepAudioCheckbox as HTMLInputElement).checked, (this.includeAudioFileLinkCheckbox as HTMLInputElement).checked);
|
2024-07-02 19:18:47 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
playPauseButton.addEventListener('click', () => {
|
|
|
|
|
if (this.isRecording) {
|
|
|
|
|
this.pauseRecording();
|
|
|
|
|
setIcon(playPauseButton, 'circle');
|
|
|
|
|
header.textContent = 'Paused';
|
|
|
|
|
modalEl.addClass('smart-memo-paused');
|
|
|
|
|
modalEl.removeClass('smart-memo-recording');
|
|
|
|
|
} else {
|
|
|
|
|
this.resumeOrStartRecording();
|
|
|
|
|
setIcon(playPauseButton, 'pause');
|
|
|
|
|
header.textContent = 'Recording...';
|
|
|
|
|
modalEl.removeClass('smart-memo-paused');
|
|
|
|
|
modalEl.addClass('smart-memo-recording');
|
|
|
|
|
}
|
|
|
|
|
this.isRecording = !this.isRecording;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const transcribeButton = controlGroupWrapper.createEl('button', { cls: 'smart-memo-modal-button smart-memo-full-width-button smart-memo-transcribe-button' });
|
|
|
|
|
|
|
|
|
|
transcribeButton.addEventListener('click', async () => {
|
|
|
|
|
const audioFile = await this.stopRecording();
|
2024-08-21 14:40:06 +00:00
|
|
|
this.handleAudioRecording(audioFile, true, (this.keepAudioCheckbox as HTMLInputElement).checked, (this.includeAudioFileLinkCheckbox as HTMLInputElement).checked);
|
2024-07-02 19:18:47 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setIcon(transcribeButton, 'file-text'); // Initially set to bulb
|
|
|
|
|
|
|
|
|
|
// Append text to the button
|
|
|
|
|
const buttonText = document.createTextNode(' Smart Transcribe');
|
|
|
|
|
transcribeButton.appendChild(buttonText);
|
|
|
|
|
|
|
|
|
|
// Add margin-right to the SVG element
|
|
|
|
|
const svgElement = transcribeButton.querySelector('svg');
|
|
|
|
|
if (svgElement) {
|
|
|
|
|
svgElement.style.marginRight = '10px';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resetButton = contentEl.createEl('button', { cls: 'smart-memo-modal-button smart-memo-full-width-button smart-memo-reset-button', text: 'Restart' });
|
|
|
|
|
resetButton.addEventListener('click', () => {
|
|
|
|
|
this.hardReset();
|
|
|
|
|
setIcon(playPauseButton, 'circle');
|
|
|
|
|
header.textContent = 'Ready to Record';
|
|
|
|
|
this.isRecording = false;
|
|
|
|
|
modalEl.addClass('smart-memo-paused');
|
|
|
|
|
modalEl.removeClass('smart-memo-recording');
|
|
|
|
|
// Ensure red dot stops pulsing
|
|
|
|
|
this.redDot.classList.remove('smart-memo-pulse-animation');
|
|
|
|
|
});
|
2024-08-09 17:20:58 +00:00
|
|
|
// Add the checkbox
|
|
|
|
|
const keepAudioContainer = contentEl.createDiv({ cls: 'smart-memo-keep-audio-container' });
|
|
|
|
|
this.keepAudioCheckbox = keepAudioContainer.createEl('input', { type: 'checkbox', cls: 'smart-memo-keep-audio-checkbox' });
|
|
|
|
|
(this.keepAudioCheckbox as HTMLInputElement).checked = this.settings.keepAudio // Set checked based on settings;
|
|
|
|
|
const keepAudioLabel = keepAudioContainer.createEl('label', { text: 'Keep Audio File', cls: 'smart-memo-keep-audio-label' });
|
|
|
|
|
keepAudioLabel.htmlFor = this.keepAudioCheckbox.id;
|
2024-07-02 19:18:47 +00:00
|
|
|
|
2024-08-21 14:40:06 +00:00
|
|
|
// Add the checkbox for including audio file link
|
|
|
|
|
const includeAudioFileLinkContainer = contentEl.createDiv({ cls: 'smart-memo-include-audio-file-link-container' });
|
|
|
|
|
this.includeAudioFileLinkCheckbox = includeAudioFileLinkContainer.createEl('input', { type: 'checkbox', cls: 'smart-memo-include-audio-file-link-checkbox' });
|
|
|
|
|
(this.includeAudioFileLinkCheckbox as HTMLInputElement).checked = this.settings.includeAudioFileLink; // Set checked based on settings
|
|
|
|
|
const includeAudioFileLinkLabel = includeAudioFileLinkContainer.createEl('label', { text: 'Include Audio File Player', cls: 'smart-memo-include-audio-file-link-label' });
|
|
|
|
|
includeAudioFileLinkLabel.htmlFor = this.includeAudioFileLinkCheckbox.id;
|
|
|
|
|
|
2024-07-02 19:18:47 +00:00
|
|
|
// Start recording immediately upon opening the modal
|
|
|
|
|
this.startRecording();
|
|
|
|
|
this.isRecording = true;
|
|
|
|
|
this.redDot.classList.add('smart-memo-pulse-animation');
|
|
|
|
|
|
|
|
|
|
// Blur any focused element
|
|
|
|
|
const activeElement = document.activeElement as HTMLElement;
|
|
|
|
|
if (activeElement) {
|
|
|
|
|
activeElement.blur();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
startRecording() {
|
|
|
|
|
navigator.mediaDevices.getUserMedia({ audio: true })
|
|
|
|
|
.then(stream => {
|
|
|
|
|
this.mediaRecorder = new MediaRecorder(stream);
|
|
|
|
|
this.setupMediaRecorder();
|
|
|
|
|
this.mediaRecorder.start(1000);
|
|
|
|
|
this.startTime = Date.now();
|
|
|
|
|
this.startTimer();
|
|
|
|
|
this.mediaRecorder.addEventListener('dataavailable', this.onDataAvailable.bind(this));
|
|
|
|
|
})
|
|
|
|
|
.catch(error => {
|
|
|
|
|
console.error('Error accessing microphone:', error);
|
|
|
|
|
this.reject(error);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setupMediaRecorder() {
|
|
|
|
|
if (this.mediaRecorder) {
|
|
|
|
|
this.mediaRecorder.addEventListener('stop', this.onStop.bind(this));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onDataAvailable(event: BlobEvent) {
|
|
|
|
|
if (this.isResetting) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.chunks.push(event.data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onStop() {
|
|
|
|
|
if (this.isResetting) {
|
|
|
|
|
this.isResetting = false; // Reset the flag after reset
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const blob = new Blob(this.chunks, { type: 'audio/wav' });
|
|
|
|
|
if (this.resolve) {
|
|
|
|
|
this.resolve(blob);
|
|
|
|
|
this.close();
|
|
|
|
|
} else {
|
|
|
|
|
console.error('Resolve function is not defined');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pauseRecording() {
|
|
|
|
|
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
|
|
|
|
|
this.mediaRecorder.pause();
|
|
|
|
|
this.stopTimer();
|
|
|
|
|
this.elapsedTime += Date.now() - this.startTime; // Accumulate elapsed time
|
|
|
|
|
|
|
|
|
|
// Ensure red dot stops pulsing
|
|
|
|
|
this.redDot.classList.remove('smart-memo-pulse-animation');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resumeOrStartRecording() {
|
|
|
|
|
if (this.mediaRecorder && this.mediaRecorder.state === 'paused') {
|
|
|
|
|
this.mediaRecorder.resume();
|
|
|
|
|
} else {
|
|
|
|
|
this.startRecording();
|
|
|
|
|
}
|
|
|
|
|
this.startTime = Date.now(); // Reset start time to now
|
|
|
|
|
this.startTimer();
|
|
|
|
|
|
|
|
|
|
// Ensure red dot starts pulsing
|
|
|
|
|
this.redDot.classList.add('smart-memo-pulse-animation');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hardReset() {
|
|
|
|
|
if (this.mediaRecorder) {
|
|
|
|
|
this.mediaRecorder.stop();
|
|
|
|
|
this.mediaRecorder.onstop = null;
|
|
|
|
|
this.mediaRecorder.ondataavailable = null;
|
|
|
|
|
this.mediaRecorder = null;
|
|
|
|
|
}
|
|
|
|
|
this.isResetting = true; // Set the reset flag
|
|
|
|
|
this.chunks = []; // Clear the chunks
|
|
|
|
|
this.elapsedTime = 0; // Reset elapsed time
|
|
|
|
|
this.stopTimer();
|
|
|
|
|
this.timer.textContent = '00:00';
|
|
|
|
|
// Ensure red dot stops pulsing
|
|
|
|
|
this.redDot.classList.remove('smart-memo-pulse-animation'); // Ensure it's removed on reset
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stopRecording() {
|
|
|
|
|
return new Promise<Blob | null>((resolve, reject) => {
|
|
|
|
|
this.resolve = resolve;
|
|
|
|
|
this.reject = reject;
|
|
|
|
|
if (this.mediaRecorder) {
|
|
|
|
|
this.mediaRecorder.addEventListener('stop', this.onStop.bind(this));
|
|
|
|
|
this.mediaRecorder.stop();
|
|
|
|
|
this.stopTimer();
|
|
|
|
|
} else {
|
|
|
|
|
resolve(null);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
startTimer() {
|
|
|
|
|
this.stopTimer(); // Clear any existing timer
|
|
|
|
|
this.intervalId = window.setInterval(() => {
|
|
|
|
|
const elapsedTimeInSeconds = Math.floor(this.elapsedTime / 1000) + Math.floor((Date.now() - this.startTime) / 1000);
|
|
|
|
|
const minutes = Math.floor(elapsedTimeInSeconds / 60);
|
|
|
|
|
const seconds = elapsedTimeInSeconds % 60;
|
|
|
|
|
this.timer.textContent = `${this.padNumber(minutes)}:${this.padNumber(seconds)}`;
|
|
|
|
|
}, 1000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stopTimer() {
|
|
|
|
|
if (this.intervalId !== null) {
|
|
|
|
|
window.clearInterval(this.intervalId);
|
|
|
|
|
this.intervalId = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
padNumber(num: number): string {
|
|
|
|
|
return num.toString().padStart(2, '0');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
open() {
|
|
|
|
|
super.open();
|
|
|
|
|
return new Promise<Blob>((resolve, reject) => {
|
|
|
|
|
this.resolve = resolve;
|
|
|
|
|
this.reject = reject;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|