diff --git a/src/App.tsx b/src/App.tsx index ba6fa2a..381b81b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import type { RenderingEvent } from './core/audio-interface/KGOfflineRenderer'; import { KGCore } from './core/KGCore'; import { ConfigManager } from './core/config/ConfigManager'; import { validateFunctionalChordsJSON } from './util/scaleUtil'; +import { showAlert } from './components/common/DialogProvider'; import { KGProjectStorage } from './core/io/KGProjectStorage'; import { RESERVED_PROJECT_NAME } from './util/projectNameUtil'; @@ -212,12 +213,11 @@ const GlobalLoadingOverlayContainer: React.FC = () => { useEffectReact(() => { // When loading starts, start a 30s timer if not already overdue/timed if (loadingCount > 0 && !overdue && timeoutRef.current === null) { - timeoutRef.current = window.setTimeout(() => { + timeoutRef.current = window.setTimeout(async () => { // Only trigger if still loading if (loadingCount > 0) { setOverdue(true); - // Friendly alert to the user - window.alert( + await showAlert( 'Loading resources is taking longer than expected and may have partially failed. If you notice any playback issues, please refresh the page to retry downloading the audio files.' ); } diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index c0d2cb0..d3cea05 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -31,6 +31,7 @@ import { clearChatHistoryAndUI } from '../util/chatUtil'; import PianoIcon from './common/icons/PianoIcon'; import MetronomeIcon from './common/icons/MetronomeIcon'; import { ConfigManager } from '../core/config/ConfigManager'; +import { showAlert, showConfirm, showPrompt } from './common/DialogProvider'; const Toolbar: React.FC = () => { const { @@ -91,14 +92,14 @@ const Toolbar: React.FC = () => { const exportOptions = ["Export to KGStudio file", "Export to MIDI file", "Export to WAV", "Export to MP3"]; const handleProjectNameClick = async () => { - const newName = prompt("Enter project name:", projectName); + const newName = await showPrompt("Enter project name:", projectName); if (!newName) return; if (!isValidProjectName(newName)) { - window.alert("Invalid project name. Only letters, numbers, spaces, hyphens, underscores, periods, and parentheses are allowed."); + await showAlert("Invalid project name. Only letters, numbers, spaces, hyphens, underscores, periods, and parentheses are allowed."); return; } if (isReservedProjectName(newName)) { - window.alert(`"${RESERVED_PROJECT_NAME}" is a reserved project name. Please choose a different name.`); + await showAlert(`"${RESERVED_PROJECT_NAME}" is a reserved project name. Please choose a different name.`); return; } @@ -107,7 +108,7 @@ const Toolbar: React.FC = () => { const storage = KGProjectStorage.getInstance(); const exists = await storage.exists(newName); if (exists) { - const confirmed = window.confirm( + const confirmed = await showConfirm( `Project "${newName}" already exists. Do you want to overwrite it?` ); if (!confirmed) return; @@ -158,7 +159,7 @@ const Toolbar: React.FC = () => { } catch (error) { console.error(`Error loading project from ${sourceDescription}:`, error); setStatus(`Failed to load project: ${error}`); - window.alert(`An error occurred while loading the project: ${error}`); + await showAlert(`An error occurred while loading the project: ${error}`); } }; @@ -183,8 +184,8 @@ const Toolbar: React.FC = () => { }; // Handler functions for file operations - const handleNewProject = () => { - const confirmed = window.confirm("Are you sure you want to create a new project? Any unsaved changes will be lost."); + const handleNewProject = async () => { + const confirmed = await showConfirm("Are you sure you want to create a new project? Any unsaved changes will be lost."); if (confirmed) { createNewProject(); } @@ -203,14 +204,14 @@ const Toolbar: React.FC = () => { const loadedProject = await storage.load(projectNameToLoad); if (!loadedProject) { - window.alert(`Project "${projectNameToLoad}" not found.`); + await showAlert(`Project "${projectNameToLoad}" not found.`); return; } await loadProjectFromData(loadedProject, `Project "${projectNameToLoad}"`, projectNameToLoad); } catch (error) { console.error("Error loading project:", error); - window.alert(`An error occurred while loading the project: ${error}`); + await showAlert(`An error occurred while loading the project: ${error}`); } }; @@ -277,11 +278,11 @@ const Toolbar: React.FC = () => { } catch (error) { console.error("Error exporting KGStudio file:", error); setStatus(`Error exporting project: ${error}`); - window.alert(`Failed to export project: ${error}`); + await showAlert(`Failed to export project: ${error}`); } }; - const handleExportMIDI = () => { + const handleExportMIDI = async () => { if (DEBUG_MODE.TOOLBAR) { console.log("exporting to MIDI file"); } @@ -319,7 +320,7 @@ const Toolbar: React.FC = () => { } catch (error) { console.error("Error exporting MIDI:", error); setStatus(`Error exporting MIDI: ${error}`); - window.alert(`Failed to export project as MIDI: ${error}`); + await showAlert(`Failed to export project as MIDI: ${error}`); } }; @@ -335,7 +336,7 @@ const Toolbar: React.FC = () => { } catch (error) { console.error("Error bouncing to WAV:", error); setStatus(`Error exporting WAV: ${error}`); - window.alert(`Failed to export project as WAV: ${error}`); + await showAlert(`Failed to export project as WAV: ${error}`); } }; @@ -351,7 +352,7 @@ const Toolbar: React.FC = () => { } catch (error) { console.error("Error bouncing to MP3:", error); setStatus(`Error exporting MP3: ${error}`); - window.alert(`Failed to export project as MP3: ${error}`); + await showAlert(`Failed to export project as MP3: ${error}`); } }; @@ -387,7 +388,7 @@ const Toolbar: React.FC = () => { } catch (error) { console.error("Error importing file:", error); setStatus(`Failed to import file: ${error}`); - window.alert(`Failed to import project file: ${error}`); + await showAlert(`Failed to import project file: ${error}`); } }; @@ -409,7 +410,7 @@ const Toolbar: React.FC = () => { console.log("KGStudio file imported successfully:", projectName); } } catch (error) { - window.alert(`The .kgstudio file is corrupted or invalid: ${error}`); + await showAlert(`The .kgstudio file is corrupted or invalid: ${error}`); throw error; } }; @@ -536,31 +537,31 @@ const Toolbar: React.FC = () => { }; // Prompt to change max bars when clicking on current-time display - const handleCurrentTimeClick = () => { + const handleCurrentTimeClick = async () => { const MIN_BARS = 16; - const newMaxBarsStr = prompt(`Enter new max bars (>= ${MIN_BARS}):`, String(maxBars ?? 32)); + const newMaxBarsStr = await showPrompt(`Enter new max bars (>= ${MIN_BARS}):`, String(maxBars ?? 32)); if (newMaxBarsStr === null) { return; // cancelled } const parsed = parseInt(newMaxBarsStr.trim(), 10); if (isNaN(parsed)) { - alert('Invalid input. Please enter a valid number.'); + await showAlert('Invalid input. Please enter a valid number.'); return; } if (parsed < MIN_BARS) { - alert(`Invalid value. Please enter a number >= ${MIN_BARS}.`); + await showAlert(`Invalid value. Please enter a number >= ${MIN_BARS}.`); return; } setMaxBars(parsed); setStatus(`Max bars changed to ${parsed}`); }; - const handleBpmClick = () => { + const handleBpmClick = async () => { if (DEBUG_MODE.TOOLBAR) { console.log("BPM clicked, current BPM:", bpm); } - const newBpmStr = prompt(`Enter new BPM (${TIME_CONSTANTS.MIN_BPM}-${TIME_CONSTANTS.MAX_BPM}):`, bpm.toString()); + const newBpmStr = await showPrompt(`Enter new BPM (${TIME_CONSTANTS.MIN_BPM}-${TIME_CONSTANTS.MAX_BPM}):`, bpm.toString()); // Check if user cancelled if (newBpmStr === null) { @@ -572,13 +573,13 @@ const Toolbar: React.FC = () => { // Check if it's a valid number if (isNaN(newBpm)) { - alert("Invalid input. Please enter a valid number."); + await showAlert("Invalid input. Please enter a valid number."); return; } // Check if it's within valid range if (newBpm <= TIME_CONSTANTS.MIN_BPM || newBpm >= TIME_CONSTANTS.MAX_BPM) { - alert(`Invalid BPM. Please enter a value between ${TIME_CONSTANTS.MIN_BPM} and ${TIME_CONSTANTS.MAX_BPM}.`); + await showAlert(`Invalid BPM. Please enter a value between ${TIME_CONSTANTS.MIN_BPM} and ${TIME_CONSTANTS.MAX_BPM}.`); return; } @@ -591,13 +592,13 @@ const Toolbar: React.FC = () => { } }; - const handleTimeSignatureClick = () => { + const handleTimeSignatureClick = async () => { if (DEBUG_MODE.TOOLBAR) { console.log("Time signature clicked, current:", `${timeSignature.numerator}/${timeSignature.denominator}`); } const currentTimeSignatureStr = `${timeSignature.numerator}/${timeSignature.denominator}`; - const newTimeSignatureStr = prompt(`Enter new time signature (numerator/denominator):`, currentTimeSignatureStr); + const newTimeSignatureStr = await showPrompt(`Enter new time signature (numerator/denominator):`, currentTimeSignatureStr); // Check if user cancelled if (newTimeSignatureStr === null) { @@ -608,7 +609,7 @@ const Toolbar: React.FC = () => { const newTimeSignature = parseTimeSignature(newTimeSignatureStr); if (newTimeSignature === null) { - alert(getTimeSignatureErrorMessage()); + await showAlert(getTimeSignatureErrorMessage()); return; } @@ -714,17 +715,17 @@ const Toolbar: React.FC = () => { }; // Handle split region button click - const handleSplitClick = () => { + const handleSplitClick = async () => { if (DEBUG_MODE.TOOLBAR) { console.log("Split button clicked"); } if (selectedRegionIds.length === 0) { - alert("Please select a region to split."); + await showAlert("Please select a region to split."); return; } if (selectedRegionIds.length > 1) { - alert("Please select exactly one region to split."); + await showAlert("Please select exactly one region to split."); return; } @@ -737,7 +738,7 @@ const Toolbar: React.FC = () => { } if (!targetRegion) { - alert("Selected region not found."); + await showAlert("Selected region not found."); return; } @@ -745,7 +746,7 @@ const Toolbar: React.FC = () => { const regionEnd = regionStart + targetRegion.getLength(); if (playheadPosition <= regionStart || playheadPosition >= regionEnd) { - alert("The playhead is not inside the selected region. Move the playhead inside the region before splitting."); + await showAlert("The playhead is not inside the selected region. Move the playhead inside the region before splitting."); return; } @@ -760,13 +761,13 @@ const Toolbar: React.FC = () => { }; // Handle undo button click - const handleUndoClick = () => { + const handleUndoClick = async () => { if (DEBUG_MODE.TOOLBAR) { console.log("Undo button clicked"); } if (!canUndo) { - alert("Nothing to undo"); + await showAlert("Nothing to undo"); return; } @@ -780,13 +781,13 @@ const Toolbar: React.FC = () => { }; // Handle redo button click - const handleRedoClick = () => { + const handleRedoClick = async () => { if (DEBUG_MODE.TOOLBAR) { console.log("Redo button clicked"); } if (!canRedo) { - alert("Nothing to redo"); + await showAlert("Nothing to redo"); return; } @@ -830,7 +831,7 @@ const Toolbar: React.FC = () => { }; // Handle Piano button click: open piano roll if closed, targeting active or selected region - const handlePianoButtonClick = () => { + const handlePianoButtonClick = async () => { if (DEBUG_MODE.TOOLBAR) { console.log('Piano button clicked'); } @@ -849,6 +850,7 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log('No active or selected region; piano roll will not open'); } + await showAlert('Please select a MIDI region to open the Piano Roll.'); return; } diff --git a/src/components/common/DialogProvider.css b/src/components/common/DialogProvider.css new file mode 100644 index 0000000..8cec586 --- /dev/null +++ b/src/components/common/DialogProvider.css @@ -0,0 +1,141 @@ +.dialog-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 10001; + backdrop-filter: blur(2px); +} + +.dialog-modal { + background-color: #2d2d2d; + border: 1px solid #3a3a3a; + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + width: 90%; + max-width: 440px; + overflow: hidden; + animation: dialogFadeIn 0.2s ease-out; +} + +@keyframes dialogFadeIn { + from { + opacity: 0; + transform: scale(0.95) translateY(-10px); + } + + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + background-color: #252525; +} + +.dialog-title { + color: #e0e0e0; + font-size: 18px; + font-weight: 600; + margin: 0; +} + +.dialog-close-btn { + background: transparent; + border: none; + color: #b0b0b0; + cursor: pointer; + padding: 8px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + transition: all 0.2s ease; +} + +.dialog-close-btn:hover { + background-color: #3a3a3a; + color: #e0e0e0; +} + +.dialog-body { + padding: 24px 20px; + background-color: #2d2d2d; +} + +.dialog-message { + color: #e0e0e0; + font-size: 14px; + line-height: 1.6; + margin: 0; + white-space: pre-wrap; + word-break: break-word; +} + +.dialog-input { + width: 100%; + margin-top: 12px; + padding: 8px 10px; + background-color: #1e1e1e; + border: 1px solid #3a3a3a; + border-radius: 6px; + color: #e0e0e0; + font-size: 14px; + outline: none; + box-sizing: border-box; + transition: border-color 0.2s ease; +} + +.dialog-input:focus { + border-color: #5a9fd4; +} + +.dialog-footer { + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 16px 20px; + background-color: #2d2d2d; +} + +.dialog-btn { + padding: 8px 20px; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + border: none; + transition: all 0.2s ease; +} + +.dialog-btn-primary { + background-color: #5a9fd4; + color: #fff; +} + +.dialog-btn-primary:hover { + background-color: #4a8fc4; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(90, 159, 212, 0.3); +} + +.dialog-btn-cancel { + background-color: #3a3a3a; + color: #b0b0b0; +} + +.dialog-btn-cancel:hover { + background-color: #4a4a4a; + color: #e0e0e0; +} \ No newline at end of file diff --git a/src/components/common/DialogProvider.tsx b/src/components/common/DialogProvider.tsx new file mode 100644 index 0000000..91804bf --- /dev/null +++ b/src/components/common/DialogProvider.tsx @@ -0,0 +1,168 @@ +import React, { useState, useCallback, useRef } from 'react'; +import './DialogProvider.css'; +import { FaTimes } from 'react-icons/fa'; + +export interface ConfirmOptions { + confirmLabel?: string; + cancelLabel?: string; +} + +export interface PromptOptions { + confirmLabel?: string; + cancelLabel?: string; + placeholder?: string; +} + +let _showAlertFn: ((message: string) => Promise) | null = null; +let _showConfirmFn: ((message: string, options?: ConfirmOptions) => Promise) | null = null; +let _showPromptFn: ((message: string, defaultValue?: string, options?: PromptOptions) => Promise) | null = null; + +export function showAlert(message: string): Promise { + if (!_showAlertFn) { + window.alert(message); + return Promise.resolve(); + } + return _showAlertFn(message); +} + +export function showConfirm(message: string, options?: ConfirmOptions): Promise { + if (!_showConfirmFn) { + return Promise.resolve(window.confirm(message)); + } + return _showConfirmFn(message, options); +} + +export function showPrompt(message: string, defaultValue?: string, options?: PromptOptions): Promise { + if (!_showPromptFn) { + return Promise.resolve(window.prompt(message, defaultValue) ); + } + return _showPromptFn(message, defaultValue, options); +} + +interface DialogInfo { + type: 'alert' | 'confirm' | 'prompt'; + message: string; + options?: ConfirmOptions | PromptOptions; + defaultValue?: string; +} + +const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [dialog, setDialog] = useState(null); + const [inputValue, setInputValue] = useState(''); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const resolveRef = useRef<((value: any) => void) | null>(null); + + const openAlert = useCallback((message: string): Promise => { + return new Promise((resolve) => { + resolveRef.current = resolve; + setDialog({ type: 'alert', message }); + }); + }, []); + + const openConfirm = useCallback((message: string, options?: ConfirmOptions): Promise => { + return new Promise((resolve) => { + resolveRef.current = resolve; + setDialog({ type: 'confirm', message, options }); + }); + }, []); + + const openPrompt = useCallback((message: string, defaultValue?: string, options?: PromptOptions): Promise => { + return new Promise((resolve) => { + resolveRef.current = resolve; + setInputValue(defaultValue ?? ''); + setDialog({ type: 'prompt', message, options, defaultValue }); + }); + }, []); + + const close = useCallback((value: unknown) => { + setDialog(null); + setInputValue(''); + if (resolveRef.current) { + resolveRef.current(value); + resolveRef.current = null; + } + }, []); + + const registered = useRef(false); + if (!registered.current) { + registered.current = true; + _showAlertFn = openAlert; + _showConfirmFn = openConfirm; + _showPromptFn = openPrompt; + } + + if (!dialog) { + return <>{children}; + } + + const isAlert = dialog.type === 'alert'; + const isPrompt = dialog.type === 'prompt'; + const promptOptions = isPrompt ? (dialog.options as PromptOptions | undefined) : undefined; + + const title = isAlert ? 'Notice' : isPrompt ? 'Input' : 'Confirm'; + + const handleOverlayClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + close(isAlert ? undefined : isPrompt ? null : false); + } + }; + + const handleCancel = () => close(isAlert ? undefined : isPrompt ? null : false); + const handleConfirm = () => close(isAlert ? undefined : isPrompt ? inputValue : true); + + return ( + <> + {children} +
+
+
+

{title}

+ +
+
+

{dialog.message}

+ {isPrompt && ( + setInputValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleConfirm(); + if (e.key === 'Escape') handleCancel(); + }} + autoFocus + /> + )} +
+
+ {!isAlert && ( + + )} + +
+
+
+ + ); +}; + +export default DialogProvider; diff --git a/src/components/common/FileImportModal.tsx b/src/components/common/FileImportModal.tsx index b57b90b..7f9a134 100644 --- a/src/components/common/FileImportModal.tsx +++ b/src/components/common/FileImportModal.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useState } from 'react'; import './FileImportModal.css'; import { FaTimes } from 'react-icons/fa'; +import { showAlert } from './DialogProvider'; interface FileImportModalProps { isVisible: boolean; @@ -41,7 +42,7 @@ const FileImportModal: React.FC = ({ e.stopPropagation(); }, []); - const handleDrop = useCallback((e: React.DragEvent) => { + const handleDrop = useCallback(async (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); @@ -56,7 +57,7 @@ const FileImportModal: React.FC = ({ onFileImport(file); onClose(); } else { - alert(`Invalid file type. Please select a file with one of these extensions: ${acceptedTypes.join(', ')}`); + await showAlert(`Invalid file type. Please select a file with one of these extensions: ${acceptedTypes.join(', ')}`); } } }, [acceptedTypes, onFileImport, onClose]); diff --git a/src/components/common/OpenProjectModal.tsx b/src/components/common/OpenProjectModal.tsx index eb1ac4f..5aaade1 100644 --- a/src/components/common/OpenProjectModal.tsx +++ b/src/components/common/OpenProjectModal.tsx @@ -3,6 +3,7 @@ import { FaTimes, FaSortUp, FaSortDown, FaCopy, FaTrash, FaUndo } from 'react-ic import { KGProjectStorage, type ProjectMeta } from '../../core/io/KGProjectStorage'; import { isValidProjectName } from '../../util/projectNameUtil'; import './OpenProjectModal.css'; +import { showAlert, showConfirm, showPrompt } from './DialogProvider'; interface OpenProjectModalProps { onClose: () => void; @@ -115,12 +116,12 @@ const OpenProjectModal: React.FC = ({ onClose, onOpenProj const handleDuplicate = async (e: React.MouseEvent, projectName: string) => { e.stopPropagation(); - const newName = window.prompt('Enter a name for the duplicated project:', projectName); + const newName = await showPrompt('Enter a name for the duplicated project:', projectName); if (!newName || newName.trim() === '') return; const trimmed = newName.trim(); if (!isValidProjectName(trimmed)) { - window.alert('Invalid project name. Only letters, numbers, spaces, hyphens, underscores, periods, and parentheses are allowed.'); + await showAlert('Invalid project name. Only letters, numbers, spaces, hyphens, underscores, periods, and parentheses are allowed.'); return; } @@ -132,7 +133,7 @@ const OpenProjectModal: React.FC = ({ onClose, onOpenProj onClose(); } catch (error) { console.error('Error duplicating project:', error); - window.alert(`Failed to duplicate project: ${error}`); + await showAlert(`Failed to duplicate project: ${error}`); } }; @@ -144,7 +145,7 @@ const OpenProjectModal: React.FC = ({ onClose, onOpenProj await fetchProjects(viewMode); } catch (error) { console.error('Error deleting project:', error); - window.alert(`Failed to delete project: ${error}`); + await showAlert(`Failed to delete project: ${error}`); } }; @@ -156,13 +157,13 @@ const OpenProjectModal: React.FC = ({ onClose, onOpenProj await fetchProjects(viewMode); } catch (error) { console.error('Error restoring project:', error); - window.alert(`Failed to restore project: ${error}`); + await showAlert(`Failed to restore project: ${error}`); } }; const handlePermanentDelete = async (e: React.MouseEvent, projectName: string) => { e.stopPropagation(); - const confirmed = window.confirm( + const confirmed = await showConfirm( `Are you sure you want to permanently delete "${projectName}"?\n\nThis operation cannot be undone.` ); if (!confirmed) return; @@ -177,7 +178,7 @@ const OpenProjectModal: React.FC = ({ onClose, onOpenProj } } catch (error) { console.error('Error permanently deleting project:', error); - window.alert(`Failed to permanently delete project: ${error}`); + await showAlert(`Failed to permanently delete project: ${error}`); } }; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index b0dc2f8..b7a19bd 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -2,4 +2,6 @@ export { default as KGDropdown } from './KGDropdown'; export { default as Playhead } from './Playhead'; export { default as FileImportModal } from './FileImportModal'; export { default as LoadingOverlay } from './LoadingOverlay'; -export { default as OpenProjectModal } from './OpenProjectModal'; \ No newline at end of file +export { default as OpenProjectModal } from './OpenProjectModal'; +export { default as DialogProvider, showAlert, showConfirm, showPrompt } from './DialogProvider'; +export type { ConfirmOptions, PromptOptions } from './DialogProvider'; \ No newline at end of file diff --git a/src/components/piano-roll/PianoRoll.tsx b/src/components/piano-roll/PianoRoll.tsx index 03cc45c..524afc8 100644 --- a/src/components/piano-roll/PianoRoll.tsx +++ b/src/components/piano-roll/PianoRoll.tsx @@ -15,6 +15,7 @@ import { ConfigManager } from '../../core/config/ConfigManager'; import { beatsToBar } from '../../util/midiUtil'; import { UpdateRegionCommand } from '../../core/commands'; import { getSuitableChords, noteNameToPitchClass } from '../../util/scaleUtil'; +import { showAlert, showPrompt } from '../common/DialogProvider'; interface PianoRollProps { onClose: () => void; @@ -250,7 +251,7 @@ const PianoRoll: React.FC = ({ }, [isDragging, isResizing, dragOffset, position]); // Handle title click to rename the region - const handleTitleClick = () => { + const handleTitleClick = async () => { // If we were just dragging, don't show the rename dialog if (wasDraggingRef.current) { if (DEBUG_MODE.PIANO_ROLL) { @@ -262,7 +263,7 @@ const PianoRoll: React.FC = ({ if (!activeRegion) return; // Show a prompt to get the new name - const newName = window.prompt("Enter a new name for the region:", activeRegion.getName()); + const newName = await showPrompt("Enter a new name for the region:", activeRegion.getName()); // If the user clicked Cancel or entered an empty string, do nothing if (!newName || newName.trim() === '' || newName === activeRegion.getName()) return; @@ -282,8 +283,7 @@ const PianoRoll: React.FC = ({ } catch (error) { console.error('Error renaming region:', error); - // Optionally show user-friendly error message - alert('Failed to rename region. Please try again.'); + await showAlert('Failed to rename region. Please try again.'); } }; diff --git a/src/components/settings/sections/ChordGuideSettings.tsx b/src/components/settings/sections/ChordGuideSettings.tsx index cb3eefb..2f8c2c5 100644 --- a/src/components/settings/sections/ChordGuideSettings.tsx +++ b/src/components/settings/sections/ChordGuideSettings.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { ConfigManager } from '../../../core/config/ConfigManager'; import { validateFunctionalChordsJSON } from '../../../util/scaleUtil'; import { KGCore } from '../../../core/KGCore'; +import { showAlert } from '../../common/DialogProvider'; const ChordGuideSettings: React.FC = () => { const [chordDefinition, setChordDefinition] = useState(''); @@ -88,7 +89,7 @@ const ChordGuideSettings: React.FC = () => { console.log('Loaded default chord template'); } catch (error) { console.error('Failed to load default template:', error); - alert('Failed to load default template. Please check the console for details.'); + await showAlert('Failed to load default template. Please check the console for details.'); } }; diff --git a/src/components/track/TrackGridPanel.tsx b/src/components/track/TrackGridPanel.tsx index 1feeb16..560ed16 100644 --- a/src/components/track/TrackGridPanel.tsx +++ b/src/components/track/TrackGridPanel.tsx @@ -13,6 +13,7 @@ import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface'; import { KGAudioRegion } from '../../core/region/KGAudioRegion'; import { generateNewRegionName } from '../../util/miscUtil'; import { KGAudioFileStorage } from '../../core/io/KGAudioFileStorage'; +import { showAlert } from '../common/DialogProvider'; import { parseMidiFirstTrackNotes } from '../../util/midiUtil'; import * as Tone from 'tone'; @@ -52,7 +53,7 @@ const TrackGridPanel: React.FC = ({ const pendingAudioImportRef = useRef<{ barNumber: number; trackIndex: number } | null>(null); // Utility function to create a region at a specific position - const createRegionAtPosition = (e: React.MouseEvent, trackIndex: number) => { + const createRegionAtPosition = async (e: React.MouseEvent, trackIndex: number) => { // Get the grid container element const gridContainer = e.currentTarget.closest('.grid-container'); if (!gridContainer) return; @@ -108,7 +109,7 @@ const TrackGridPanel: React.FC = ({ if (DEBUG_MODE.TRACK_GRID_PANEL) { console.log(`Cannot create region at bar ${barNumber}: overlaps with existing region`); } - alert('Cannot create region: overlaps with existing region'); + await showAlert('Cannot create region: overlaps with existing region'); return; // Don't create the region if it overlaps } @@ -515,7 +516,7 @@ const TrackGridPanel: React.FC = ({ try { if (track.getType() === TrackType.MIDI) { if (!dropData.midiUrl) { - window.alert( + await showAlert( 'This audio clip can only be imported into an audio track.\n' + 'Please drag it onto an audio track instead.' ); diff --git a/src/components/track/TrackInfoItem.tsx b/src/components/track/TrackInfoItem.tsx index 22171df..b6c7b96 100644 --- a/src/components/track/TrackInfoItem.tsx +++ b/src/components/track/TrackInfoItem.tsx @@ -12,6 +12,7 @@ import { FLUIDR3_INSTRUMENT_MAP } from '../../constants/generalMidiConstants'; import { DEBUG_MODE } from '../../constants/uiConstants'; import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface'; import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants'; +import { showAlert, showConfirm, showPrompt } from '../common/DialogProvider'; interface TrackInfoItemProps { track: KGTrack; index: number; @@ -97,10 +98,10 @@ const TrackInfoItem: React.FC = ({ }, [allTracks, track]); // Handle track name edit within the component - const handleTrackNameClick = (e: React.MouseEvent) => { + const handleTrackNameClick = async (e: React.MouseEvent) => { e.stopPropagation(); // Prevent opening piano roll when clicking track name - - const newName = prompt("Enter track name:", track.getName()); + + const newName = await showPrompt("Enter track name:", track.getName()); if (newName) { // Call the parent handler with the new name onTrackNameEdit(track, newName); @@ -233,7 +234,7 @@ const TrackInfoItem: React.FC = ({ // Handle settings action const handleSettingsAction = async (action: string) => { if (action === 'Delete Track') { - const confirmed = window.confirm(`Are you sure you want to delete track "${track.getName()}"?`); + const confirmed = await showConfirm(`Are you sure you want to delete track "${track.getName()}"?`); if (confirmed) { try { if (DEBUG_MODE.TRACK_INFO) { @@ -252,7 +253,7 @@ const TrackInfoItem: React.FC = ({ setShowSettingsDropdown(false); } catch (error) { console.error('Failed to delete track:', error); - alert('Failed to delete track. Please try again.'); + await showAlert('Failed to delete track. Please try again.'); } } } diff --git a/src/main.tsx b/src/main.tsx index 2f9bf21..7ef77d8 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,6 +4,7 @@ import { createRoot } from 'react-dom/client'; import './styles/variables.css'; import './index.css'; import App from './App.tsx'; +import DialogProvider from './components/common/DialogProvider'; import { KGCore } from './core/KGCore'; import { KGAudioInterface } from './core/audio-interface/KGAudioInterface'; import { KGMidiInput } from './core/midi-input/KGMidiInput'; @@ -132,9 +133,11 @@ window.addEventListener('beforeunload', (event) => { // Note: Custom messages are no longer supported in modern browsers for security reasons }); -root.render( - - - , -); + root.render( + + + + + , + ); } // end isSecureContext else diff --git a/src/util/saveUtil.ts b/src/util/saveUtil.ts index 465a318..690db4c 100644 --- a/src/util/saveUtil.ts +++ b/src/util/saveUtil.ts @@ -1,6 +1,7 @@ import { KGProjectStorage } from '../core/io/KGProjectStorage'; import { KGCore } from '../core/KGCore'; import { RESERVED_PROJECT_NAME } from './projectNameUtil'; +import { showAlert } from '../components/common/DialogProvider'; /** * Save project utility function. @@ -59,7 +60,7 @@ export const saveProject = async ( return true; } catch (error) { console.error('Error saving renamed project:', error); - window.alert(`An error occurred while saving: ${error}`); + await showAlert(`An error occurred while saving: ${error}`); return false; } } @@ -72,7 +73,7 @@ export const saveProject = async ( return true; } catch (error) { console.error('Error saving project:', error); - window.alert(`An error occurred while saving: ${error}`); + await showAlert(`An error occurred while saving: ${error}`); return false; } };