From c4c5906daf9a4ea641448f76a7a348b31f09ec0c Mon Sep 17 00:00:00 2001 From: aewing Date: Sat, 28 Mar 2026 19:59:07 +0000 Subject: [PATCH] Add audio recording workflow to notes workspace --- notes-workspace.html | 152 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/notes-workspace.html b/notes-workspace.html index 5567b9a..a1a4641 100644 --- a/notes-workspace.html +++ b/notes-workspace.html @@ -176,6 +176,15 @@
+
+
+ + + + Ready +
+ +
@@ -242,11 +251,21 @@ const pendingAttachmentsList = document.getElementById('pendingAttachmentsList'); const selectedAttachmentsStatus = document.getElementById('selectedAttachmentsStatus'); const selectedAttachmentsList = document.getElementById('selectedAttachmentsList'); + const recordStatus = document.getElementById('recordStatus'); + const recordPreview = document.getElementById('recordPreview'); + const startRecordBtn = document.getElementById('startRecordBtn'); + const stopRecordBtn = document.getElementById('stopRecordBtn'); + const clearRecordBtn = document.getElementById('clearRecordBtn'); const saveBtn = document.getElementById('saveBtn'); const hideBtn = document.getElementById('hideBtn'); const deleteBtn = document.getElementById('deleteBtn'); + let recorderStream = null; + let mediaRecorder = null; + let recordChunks = []; + let lastRecordedFile = null; + function writeOutput(value) { output.textContent = typeof value === 'string' ? value : JSON.stringify(value, null, 2); } @@ -385,6 +404,101 @@ return created; } + async function ensureRecorderStream() { + if (recorderStream) return recorderStream; + recorderStream = await navigator.mediaDevices.getUserMedia({ + audio: { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + }, + }); + return recorderStream; + } + + function stopRecorderTracks() { + if (recorderStream) { + recorderStream.getTracks().forEach((t) => t.stop()); + } + recorderStream = null; + } + + async function startAudioRecording() { + if (mediaRecorder && mediaRecorder.state === 'recording') return; + + const stream = await ensureRecorderStream(); + recordChunks = []; + mediaRecorder = new MediaRecorder(stream); + + mediaRecorder.ondataavailable = (event) => { + if (event.data && event.data.size > 0) { + recordChunks.push(event.data); + } + }; + + mediaRecorder.start(); + startRecordBtn.disabled = true; + stopRecordBtn.disabled = false; + recordStatus.textContent = 'Recording…'; + } + + async function stopAudioRecording() { + if (!mediaRecorder || mediaRecorder.state !== 'recording') return; + + await new Promise((resolve) => { + mediaRecorder.addEventListener('stop', resolve, { once: true }); + mediaRecorder.stop(); + }); + + const mimeType = mediaRecorder?.mimeType || 'audio/webm'; + const blob = new Blob(recordChunks, { type: mimeType }); + stopRecorderTracks(); + mediaRecorder = null; + startRecordBtn.disabled = false; + stopRecordBtn.disabled = true; + + if (!blob.size) { + recordStatus.textContent = 'No audio captured.'; + return; + } + + const ext = mimeType.includes('ogg') ? 'ogg' : mimeType.includes('mp4') ? 'm4a' : 'webm'; + const fileName = `note-audio-${new Date().toISOString().replace(/[:.]/g, '-')}.${ext}`; + const file = new File([blob], fileName, { type: mimeType }); + pendingAttachments.push(file); + lastRecordedFile = file; + renderPendingAttachments(); + + if (recordPreview.src) URL.revokeObjectURL(recordPreview.src); + recordPreview.src = URL.createObjectURL(blob); + recordPreview.classList.remove('hidden'); + recordStatus.textContent = `Recorded: ${fileName}`; + } + + async function clearRecordedAudio() { + if (mediaRecorder && mediaRecorder.state === 'recording') { + await stopAudioRecording(); + } + + if (recordPreview.src) { + URL.revokeObjectURL(recordPreview.src); + } + recordPreview.src = ''; + recordPreview.classList.add('hidden'); + + if (lastRecordedFile) { + pendingAttachments = pendingAttachments.filter((f) => !(f.name === lastRecordedFile.name && f.size === lastRecordedFile.size)); + renderPendingAttachments(); + } + + lastRecordedFile = null; + recordStatus.textContent = 'Ready'; + stopRecorderTracks(); + mediaRecorder = null; + startRecordBtn.disabled = false; + stopRecordBtn.disabled = true; + } + async function fetchAttachmentsForNote(noteId) { try { const atts = await pb.collection(ATTACHMENTS_COLLECTION).getFullList({ @@ -624,6 +738,13 @@ shareInput.value = ''; pendingAttachments = []; renderPendingAttachments(); + if (recordPreview.src) { + URL.revokeObjectURL(recordPreview.src); + } + recordPreview.src = ''; + recordPreview.classList.add('hidden'); + recordStatus.textContent = 'Ready'; + lastRecordedFile = null; selectedAttachmentsStatus.textContent = 'Select a note to view attachments.'; selectedAttachmentsList.innerHTML = ''; saveBtn.disabled = true; @@ -641,6 +762,13 @@ shareInput.value = normalizeShares(note.shared_with).join(', '); pendingAttachments = []; renderPendingAttachments(); + if (recordPreview.src) { + URL.revokeObjectURL(recordPreview.src); + } + recordPreview.src = ''; + recordPreview.classList.add('hidden'); + recordStatus.textContent = 'Ready'; + lastRecordedFile = null; saveBtn.disabled = false; hideBtn.disabled = false; deleteBtn.disabled = false; @@ -841,6 +969,30 @@ renderPendingAttachments(); }); + startRecordBtn.addEventListener('click', async () => { + try { + await startAudioRecording(); + } catch (error) { + recordStatus.textContent = `Recorder error: ${error?.message || String(error)}`; + } + }); + + stopRecordBtn.addEventListener('click', async () => { + try { + await stopAudioRecording(); + } catch (error) { + recordStatus.textContent = `Stop failed: ${error?.message || String(error)}`; + } + }); + + clearRecordBtn.addEventListener('click', async () => { + try { + await clearRecordedAudio(); + } catch (error) { + recordStatus.textContent = `Clear failed: ${error?.message || String(error)}`; + } + }); + document.getElementById('clearBtn').addEventListener('click', () => { clearForm(); renderList();