const { ipcRenderer } = require('electron'); // const path = require('path'); const WaveSurfer = require('wavesurfer.js'); const RegionsPlugin = require('wavesurfer.js/dist/plugin/wavesurfer.regions.js'); document.addEventListener('DOMContentLoaded', async () => { // Settings Modal Logic const settingsModal = document.getElementById('settings-modal'); const settingsBtn = document.getElementById('settings-btn'); const restartBtn = document.getElementById('restart-btn'); const closeModalBtn = document.querySelector('.close-modal'); const saveSettingsBtn = document.getElementById('save-settings'); const selectOutputFolderBtn = document.getElementById('select-output-folder'); const recordingLengthInput = document.getElementById('recording-length'); const oscPortInput = document.getElementById('osc-port'); const outputFolderInput = document.getElementById('output-folder'); const inputDeviceSelect = document.getElementById('input-device'); // Open settings modal settingsBtn.addEventListener('click', async () => { try { // Request microphone permissions first await navigator.mediaDevices.getUserMedia({ audio: true }); // Load current settings const settings = await ipcRenderer.invoke('load-settings'); // Populate input devices const devices = await ipcRenderer.invoke('get-input-devices'); if (devices.length === 0) { inputDeviceSelect.innerHTML = ''; } else { inputDeviceSelect.innerHTML = devices .map( (device) => ``, ) .join(''); } // Set current settings recordingLengthInput.value = settings.recordingLength; outputFolderInput.value = settings.outputFolder; inputDeviceSelect.value = settings.inputDevice; oscPortInput.value = settings.oscPort; settingsModal.style.display = 'block'; } catch (error) { console.error('Error loading settings or devices:', error); alert('Please grant microphone permissions to list audio devices'); } }); restartBtn.addEventListener('click', async () => { try { await ipcRenderer.invoke('restart'); } catch (error) { console.error('Error restarting:', error); alert('Failed to restart Clipper'); } }); // Close settings modal closeModalBtn.addEventListener('click', () => { settingsModal.style.display = 'none'; }); // Select output folder selectOutputFolderBtn.addEventListener('click', async () => { const folderPath = await ipcRenderer.invoke('select-output-folder'); if (folderPath) { outputFolderInput.value = folderPath; } }); // Save settings saveSettingsBtn.addEventListener('click', async () => { const settings = { recordingLength: parseInt(recordingLengthInput.value), oscPort: parseInt(oscPortInput.value), outputFolder: outputFolderInput.value, inputDevice: inputDeviceSelect.value, }; const saved = await ipcRenderer.invoke('save-settings', settings); if (saved) { settingsModal.style.display = 'none'; } else { alert('Failed to save settings'); } }); // Close modal if clicked outside window.addEventListener('click', (event) => { if (event.target === settingsModal) { settingsModal.style.display = 'none'; } }); const audioTrimmersList = document.getElementById('audio-trimmers-list'); const collectionsList = document.getElementById('collections-list'); //const currentSectionTitle = document.getElementById("current-section-title"); // Global state to persist wavesurfer instances and trimmer states const globalState = { wavesurferInstances: {}, trimmerStates: {}, currentSection: 'untrimmed', trimmerElements: {}, }; // Utility function to format time function formatTime(seconds) { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; } // Populate collections list async function populateCollectionsList() { const collections = await ipcRenderer.invoke('get-collections'); collectionsList.innerHTML = ''; // Always add Untrimmed section first const untrimmedItem = document.createElement('div'); untrimmedItem.classList.add('collection-item'); untrimmedItem.textContent = 'Untrimmed'; untrimmedItem.dataset.collection = 'untrimmed'; untrimmedItem.addEventListener('click', () => { loadCollectionFiles('untrimmed'); }); collectionsList.appendChild(untrimmedItem); // Add other collections collections.forEach((collection) => { if (collection === 'untrimmed') { return; } const collectionItem = document.createElement('div'); collectionItem.classList.add('collection-item'); collectionItem.textContent = collection; collectionItem.dataset.collection = collection; collectionItem.addEventListener('click', () => { loadCollectionFiles(collection); }); collectionsList.appendChild(collectionItem); }); } // Modify loadCollectionFiles function async function loadCollectionFiles(collection) { if (collection !== globalState.currentSection) { //Clear existing trimmers and reset global state Object.keys(globalState.trimmerElements).forEach((filePath) => { const trimmerElement = globalState.trimmerElements[filePath]; if (trimmerElement && trimmerElement.parentNode) { trimmerElement.parentNode.removeChild(trimmerElement); } }); // Reset global state globalState.trimmerElements = {}; globalState.wavesurferInstances = {}; globalState.trimmerStates = {}; } // Reset active states document.querySelectorAll('.collection-item').forEach((el) => { el.classList.remove('active'); }); // Set active state only for existing items const activeItem = document.querySelector( `.collection-item[data-collection="${collection}"]`, ); // Only add active class if the item exists if (activeItem) { activeItem.classList.add('active'); } // Update section title and global state //currentSectionTitle.textContent = collection; globalState.currentSection = collection; // Load files const files = await ipcRenderer.invoke('get-collection-files', collection); // Add new trimmers with saved trim information for (const file of files) { const filePath = file.originalPath || file.fileName; // If loading a collection, use saved trim information //if (collection !== "untrimmed") { // Store trim information in global state before creating trimmer // globalState.trimmerStates[filePath] = { // trimStart: file.trimStart || 0, // trimEnd: file.trimEnd || 0, // regionStart: file.trimStart || 0, // regionEnd: file.trimEnd || 0, // originalPath: file.originalPath, // }; //} createAudioTrimmer(filePath, collection); } } // Create audio trimmer for a single file async function createAudioTrimmer(filePath, section) { // Check if trimmer already exists if (globalState.trimmerElements[filePath]) { return globalState.trimmerElements[filePath]; } const savedTrimInfo = await ipcRenderer.invoke( 'get-trim-info', globalState.currentSection, path.basename(filePath), ); // Create trimmer container const trimmerContainer = document.createElement('div'); trimmerContainer.classList.add('audio-trimmer-item'); trimmerContainer.dataset.filepath = filePath; // Create header with title and controls const trimmerHeader = document.createElement('div'); trimmerHeader.classList.add('audio-trimmer-header'); // Title container const titleContainer = document.createElement('div'); titleContainer.classList.add('audio-trimmer-title-container'); if (savedTrimInfo.title) { // Title const title = document.createElement('div'); title.classList.add('audio-trimmer-title'); title.textContent = savedTrimInfo.title; titleContainer.appendChild(title); // Filename const fileName = document.createElement('div'); fileName.classList.add('audio-trimmer-filename'); fileName.textContent = path.basename(filePath); titleContainer.appendChild(fileName); } else { // Title (using filename if no custom title) const title = document.createElement('div'); title.classList.add('audio-trimmer-title'); title.textContent = path.basename(filePath); titleContainer.appendChild(title); // Filename const fileName = document.createElement('div'); fileName.classList.add('audio-trimmer-filename'); fileName.textContent = 'hidden'; fileName.style.opacity = 0; titleContainer.appendChild(fileName); } // Controls container const controlsContainer = document.createElement('div'); controlsContainer.classList.add('audio-trimmer-controls'); // Play/Pause and Save buttons const playPauseBtn = document.createElement('button'); playPauseBtn.classList.add('play-pause-btn'); playPauseBtn.innerHTML = ` `; const saveTrimButton = document.createElement('button'); saveTrimButton.classList.add('save-trim'); saveTrimButton.innerHTML = ` `; const deletebutton = document.createElement('button'); deletebutton.classList.add('play-pause-btn'); deletebutton.innerHTML = ` `; controlsContainer.appendChild(playPauseBtn); controlsContainer.appendChild(saveTrimButton); controlsContainer.appendChild(deletebutton); // Assemble header trimmerHeader.appendChild(titleContainer); trimmerHeader.appendChild(controlsContainer); trimmerContainer.appendChild(trimmerHeader); // Waveform container const waveformContainer = document.createElement('div'); waveformContainer.classList.add('waveform-container'); const waveformId = `waveform-${path.basename( filePath, path.extname(filePath), )}`; waveformContainer.innerHTML = `
`; trimmerContainer.appendChild(waveformContainer); // Time displays const timeInfo = document.createElement('div'); timeInfo.classList.add('trim-info'); timeInfo.innerHTML = `
Start: 0:00
End: 0:00
`; // const zoomContainer = document.createElement('div'); // zoomContainer.className = 'zoom-controls'; // zoomContainer.innerHTML = ` // // // // `; // timeInfo.appendChild(zoomContainer); // const zoomInBtn = zoomContainer.querySelector('.zoom-in'); // const zoomOutBtn = zoomContainer.querySelector('.zoom-out'); // const zoomSlider = zoomContainer.querySelector('.zoom-slider'); // // Zoom functionality // const updateZoom = (zoomLevel) => { // // Get the current scroll position and width // const scrollContainer = wavesurfer.container.querySelector('wave'); // const currentScroll = scrollContainer.scrollLeft; // const containerWidth = scrollContainer.clientWidth; // // Calculate the center point of the current view // //const centerTime = wavesurfer.getCurrentTime(); // // Apply zoom // wavesurfer.zoom(zoomLevel); // // Recalculate scroll to keep the center point in view // const newDuration = wavesurfer.getDuration(); // const pixelsPerSecond = wavesurfer.drawer.width / newDuration; // const centerPixel = centerTime * pixelsPerSecond; // // Adjust scroll to keep the center point in the same relative position // const newScrollLeft = centerPixel - (containerWidth / 2); // scrollContainer.scrollLeft = Math.max(0, newScrollLeft); // console.log(currentScroll, newScrollLeft); // }; // zoomInBtn.addEventListener('click', () => { // const currentZoom = parseInt(zoomSlider.value); // zoomSlider.value = Math.min(currentZoom + 20, 200); // updateZoom(zoomSlider.value); // }); // zoomOutBtn.addEventListener('click', () => { // const currentZoom = parseInt(zoomSlider.value); // zoomSlider.value = Math.max(currentZoom - 20, 1); // updateZoom(zoomSlider.value); // }); // zoomSlider.addEventListener('input', (e) => { // updateZoom(e.target.value); // }); trimmerContainer.appendChild(timeInfo); // Add to list and global state audioTrimmersList.appendChild(trimmerContainer); globalState.trimmerElements[filePath] = trimmerContainer; // Determine the file to load (original or current) const fileToLoad = section === 'untrimmed' ? filePath : globalState.trimmerStates[filePath]?.originalPath || filePath; // Setup wavesurfer const wavesurfer = WaveSurfer.create({ container: `#${waveformId}`, waveColor: '#ccb1ff', progressColor: '#6e44ba', responsive: true, height: 100, hideScrollbar: true, // barWidth: 2, // barRadius: 3, cursorWidth: 1, backend: 'WebAudio', plugins: [ RegionsPlugin.create({ color: 'rgba(132, 81, 224, 0.3)', drag: false, resize: true, dragSelection: { slop: 20, }, }), // ZoomPlugin.create({ // // the amount of zoom per wheel step, e.g. 0.5 means a 50% magnification per scroll // scale: 0.5, // // Optionally, specify the maximum pixels-per-second factor while zooming // maxZoom: 100, // }), ], }); // Store wavesurfer instance in global state globalState.wavesurferInstances[filePath] = wavesurfer; // Use existing trim state or create new one globalState.trimmerStates[filePath] = globalState.trimmerStates[filePath] || savedTrimInfo || { trimStart: 0, trimEnd: 0, regionStart: undefined, regionEnd: undefined, originalPath: fileToLoad, }; const startTimeDisplay = timeInfo.querySelector('.trim-start-time'); const endTimeDisplay = timeInfo.querySelector('.trim-end-time'); // Load audio file wavesurfer.load(`file://${fileToLoad}`); // Setup play/pause button playPauseBtn.onclick = () => { const instanceState = globalState.trimmerStates[filePath]; if (wavesurfer.isPlaying()) { wavesurfer.pause(); playPauseBtn.innerHTML = ` `; } else { // Always start from the trim start wavesurfer.play(instanceState.trimStart, instanceState.trimEnd); playPauseBtn.innerHTML = ` `; } }; // When audio is ready wavesurfer.on('ready', async () => { const instanceState = globalState.trimmerStates[filePath]; // Set trim times based on saved state or full duration if (instanceState.trimStart) { // Create initial region covering trim or full duration wavesurfer.clearRegions(); const region = wavesurfer.addRegion({ start: instanceState.trimStart, end: instanceState.trimEnd, color: 'rgba(132, 81, 224, 0.3)', drag: false, resize: true, }); } instanceState.trimStart = instanceState.trimStart || 0; instanceState.trimEnd = instanceState.trimEnd || wavesurfer.getDuration(); // Update time displays startTimeDisplay.textContent = formatTime(instanceState.trimStart); endTimeDisplay.textContent = formatTime(instanceState.trimEnd); // Store region details instanceState.regionStart = instanceState.trimStart; instanceState.regionEnd = instanceState.trimEnd; // Listen for region updates wavesurfer.on('region-update-end', async (updatedRegion) => { // Ensure the region doesn't exceed audio duration instanceState.trimStart = Math.max(0, updatedRegion.start); instanceState.trimEnd = Math.min( wavesurfer.getDuration(), updatedRegion.end, ); // Update time displays startTimeDisplay.textContent = formatTime(instanceState.trimStart); endTimeDisplay.textContent = formatTime(instanceState.trimEnd); // Store updated region details instanceState.regionStart = instanceState.trimStart; instanceState.regionEnd = instanceState.trimEnd; globalState.trimmerStates[filePath] = instanceState; // Adjust region if it exceeds bounds updatedRegion.update({ start: instanceState.trimStart, end: instanceState.trimEnd, }); }); // Handle region creation wavesurfer.on('region-created', (newRegion) => { // Remove all other regions Object.keys(wavesurfer.regions.list).forEach((id) => { if (wavesurfer.regions.list[id] !== newRegion) { wavesurfer.regions.list[id].remove(); } }); }); // Reset to trim start when audio finishes wavesurfer.on('finish', () => { wavesurfer.setCurrentTime(instanceState.trimStart); playPauseBtn.innerHTML = ` `; }); // Save trimmed audio functionality saveTrimButton.addEventListener('click', async () => { try { // Get current collections const collections = await ipcRenderer.invoke('get-collections'); // Create a dialog to select or create a collection const dialogHtml = `
`; // Create dialog overlay const overlay = document.createElement('div'); overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.backgroundColor = 'rgba(0,0,0,0.5)'; overlay.style.zIndex = '999'; overlay.innerHTML = dialogHtml; document.body.appendChild(overlay); const existingCollectionsSelect = overlay.querySelector( '#existing-collections', ); const newSaveTitleInput = overlay.querySelector('#new-save-title'); const createCollectionBtn = overlay.querySelector( '#create-collection-btn', ); const saveToCollectionBtn = overlay.querySelector( '#save-to-collection-btn', ); const cancelSaveBtn = overlay.querySelector('#cancel-save-btn'); if (savedTrimInfo.title) { newSaveTitleInput.value = savedTrimInfo.title; } // Save to collection saveToCollectionBtn.addEventListener('click', async () => { const newTitle = document .getElementById('new-save-title') .value.trim(); const settings = await ipcRenderer.invoke('load-settings'); const selectedCollection = existingCollectionsSelect.value; if (!selectedCollection) { alert('Please select or create a collection'); return; } try { await ipcRenderer.invoke( 'delete-old-file', settings.outputFolder, globalState.currentSection, savedTrimInfo.title, ); await ipcRenderer.invoke( 'save-trimmed-file', path.basename(filePath), globalState.currentSection, selectedCollection, instanceState.trimStart, instanceState.trimEnd, newTitle, ); const saveResult = await ipcRenderer.invoke( 'save-trimmed-audio', { originalFilePath: filePath, outputFolder: settings.outputFolder, collectionName: selectedCollection, title: newTitle, trimStart: instanceState.trimStart, trimEnd: instanceState.trimEnd, }, ); if (saveResult.success) { // Close save dialog // Remove dialog document.body.removeChild(overlay); trimmerContainer.remove(); await loadCollectionFiles(globalState.currentSection); await populateCollectionsList(); // Optional: Show success message //alert(`Trimmed audio saved to ${saveResult.filePath}`); } else { alert(`Failed to save trimmed audio: ${saveResult.error}`); } // Refresh the view } catch (error) { alert('Error saving file: ' + error.message); } }); // Cancel button cancelSaveBtn.addEventListener('click', () => { document.body.removeChild(overlay); }); } catch (error) { console.error('Error creating save dialog:', error); } }); deletebutton.addEventListener('click', async () => { // Create confirmation dialog const confirmDelete = confirm( `Are you sure you want to delete this audio file?\nThis will remove the original file and any trimmed versions.`, ); if (confirmDelete) { try { // Delete original file await ipcRenderer.invoke('delete-file', filePath); // Remove from UI trimmerContainer.remove(); // Optional: Notify user alert('File deleted successfully'); // Refresh the current section view await loadCollectionFiles(globalState.currentSection); await populateCollectionsList(); } catch (error) { console.error('Error deleting file:', error); } } }); }); return trimmerContainer; } // Initial load of untrimmed files and collections await loadCollectionFiles('untrimmed'); await populateCollectionsList(); // Listen for new untrimmed files ipcRenderer.on('new-untrimmed-file', async (event, filePath) => { // Refresh the untrimmed section await loadCollectionFiles('untrimmed'); await populateCollectionsList(); }); // Periodic refresh setInterval(async () => { await populateCollectionsList(); await loadCollectionFiles(globalState.currentSection); }, 5000); }); // Add collection button handler document .getElementById('add-collection-btn') .addEventListener('click', async () => { try { // Create a dialog to input new collection name const dialogHtml = `

Create New Collection

`; // Create dialog overlay const overlay = document.createElement('div'); overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.backgroundColor = 'rgba(0,0,0,0.5)'; overlay.style.zIndex = '999'; overlay.innerHTML = dialogHtml; document.body.appendChild(overlay); const newCollectionInput = overlay.querySelector('#new-collection-input'); const createCollectionConfirmBtn = overlay.querySelector( '#create-collection-confirm-btn', ); const createCollectionCancelBtn = overlay.querySelector( '#create-collection-cancel-btn', ); // Create collection when confirm button is clicked createCollectionConfirmBtn.addEventListener('click', async () => { const newCollectionName = newCollectionInput.value.trim(); if (newCollectionName) { try { await ipcRenderer.invoke('add-new-collection', newCollectionName); // Remove dialog document.body.removeChild(overlay); // Refresh collections list await populateCollectionsList(); } catch (error) { // Show error in the dialog const errorDiv = document.createElement('div'); errorDiv.textContent = error.message; errorDiv.style.color = 'red'; errorDiv.style.marginTop = '10px'; overlay.querySelector('div').appendChild(errorDiv); } } else { // Show error if input is empty const errorDiv = document.createElement('div'); errorDiv.textContent = 'Collection name cannot be empty'; errorDiv.style.color = 'red'; errorDiv.style.marginTop = '10px'; overlay.querySelector('div').appendChild(errorDiv); } }); // Cancel button closes the dialog createCollectionCancelBtn.addEventListener('click', () => { document.body.removeChild(overlay); }); // Focus the input when dialog opens newCollectionInput.focus(); } catch (error) { console.error('Error creating new collection dialog:', error); } });