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();