feat: replace browser built-in alert/confirm/prompt pop-up with new DialogProvider

This commit is contained in:
Xiaohan-Tian
2026-04-19 23:54:26 -07:00
parent 0ac39068b2
commit 1828dbbc63
13 changed files with 392 additions and 70 deletions
+3 -3
View File
@@ -18,6 +18,7 @@ import type { RenderingEvent } from './core/audio-interface/KGOfflineRenderer';
import { KGCore } from './core/KGCore'; import { KGCore } from './core/KGCore';
import { ConfigManager } from './core/config/ConfigManager'; import { ConfigManager } from './core/config/ConfigManager';
import { validateFunctionalChordsJSON } from './util/scaleUtil'; import { validateFunctionalChordsJSON } from './util/scaleUtil';
import { showAlert } from './components/common/DialogProvider';
import { KGProjectStorage } from './core/io/KGProjectStorage'; import { KGProjectStorage } from './core/io/KGProjectStorage';
import { RESERVED_PROJECT_NAME } from './util/projectNameUtil'; import { RESERVED_PROJECT_NAME } from './util/projectNameUtil';
@@ -212,12 +213,11 @@ const GlobalLoadingOverlayContainer: React.FC = () => {
useEffectReact(() => { useEffectReact(() => {
// When loading starts, start a 30s timer if not already overdue/timed // When loading starts, start a 30s timer if not already overdue/timed
if (loadingCount > 0 && !overdue && timeoutRef.current === null) { if (loadingCount > 0 && !overdue && timeoutRef.current === null) {
timeoutRef.current = window.setTimeout(() => { timeoutRef.current = window.setTimeout(async () => {
// Only trigger if still loading // Only trigger if still loading
if (loadingCount > 0) { if (loadingCount > 0) {
setOverdue(true); setOverdue(true);
// Friendly alert to the user await showAlert(
window.alert(
'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.' '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.'
); );
} }
+39 -37
View File
@@ -31,6 +31,7 @@ import { clearChatHistoryAndUI } from '../util/chatUtil';
import PianoIcon from './common/icons/PianoIcon'; import PianoIcon from './common/icons/PianoIcon';
import MetronomeIcon from './common/icons/MetronomeIcon'; import MetronomeIcon from './common/icons/MetronomeIcon';
import { ConfigManager } from '../core/config/ConfigManager'; import { ConfigManager } from '../core/config/ConfigManager';
import { showAlert, showConfirm, showPrompt } from './common/DialogProvider';
const Toolbar: React.FC = () => { const Toolbar: React.FC = () => {
const { 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 exportOptions = ["Export to KGStudio file", "Export to MIDI file", "Export to WAV", "Export to MP3"];
const handleProjectNameClick = async () => { const handleProjectNameClick = async () => {
const newName = prompt("Enter project name:", projectName); const newName = await showPrompt("Enter project name:", projectName);
if (!newName) return; if (!newName) return;
if (!isValidProjectName(newName)) { 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; return;
} }
if (isReservedProjectName(newName)) { 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; return;
} }
@@ -107,7 +108,7 @@ const Toolbar: React.FC = () => {
const storage = KGProjectStorage.getInstance(); const storage = KGProjectStorage.getInstance();
const exists = await storage.exists(newName); const exists = await storage.exists(newName);
if (exists) { if (exists) {
const confirmed = window.confirm( const confirmed = await showConfirm(
`Project "${newName}" already exists. Do you want to overwrite it?` `Project "${newName}" already exists. Do you want to overwrite it?`
); );
if (!confirmed) return; if (!confirmed) return;
@@ -158,7 +159,7 @@ const Toolbar: React.FC = () => {
} catch (error) { } catch (error) {
console.error(`Error loading project from ${sourceDescription}:`, error); console.error(`Error loading project from ${sourceDescription}:`, error);
setStatus(`Failed to load project: ${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 // Handler functions for file operations
const handleNewProject = () => { const handleNewProject = async () => {
const confirmed = window.confirm("Are you sure you want to create a new project? Any unsaved changes will be lost."); const confirmed = await showConfirm("Are you sure you want to create a new project? Any unsaved changes will be lost.");
if (confirmed) { if (confirmed) {
createNewProject(); createNewProject();
} }
@@ -203,14 +204,14 @@ const Toolbar: React.FC = () => {
const loadedProject = await storage.load(projectNameToLoad); const loadedProject = await storage.load(projectNameToLoad);
if (!loadedProject) { if (!loadedProject) {
window.alert(`Project "${projectNameToLoad}" not found.`); await showAlert(`Project "${projectNameToLoad}" not found.`);
return; return;
} }
await loadProjectFromData(loadedProject, `Project "${projectNameToLoad}"`, projectNameToLoad); await loadProjectFromData(loadedProject, `Project "${projectNameToLoad}"`, projectNameToLoad);
} catch (error) { } catch (error) {
console.error("Error loading project:", 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) { } catch (error) {
console.error("Error exporting KGStudio file:", error); console.error("Error exporting KGStudio file:", error);
setStatus(`Error exporting project: ${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) { if (DEBUG_MODE.TOOLBAR) {
console.log("exporting to MIDI file"); console.log("exporting to MIDI file");
} }
@@ -319,7 +320,7 @@ const Toolbar: React.FC = () => {
} catch (error) { } catch (error) {
console.error("Error exporting MIDI:", error); console.error("Error exporting MIDI:", error);
setStatus(`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) { } catch (error) {
console.error("Error bouncing to WAV:", error); console.error("Error bouncing to WAV:", error);
setStatus(`Error exporting 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) { } catch (error) {
console.error("Error bouncing to MP3:", error); console.error("Error bouncing to MP3:", error);
setStatus(`Error exporting 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) { } catch (error) {
console.error("Error importing file:", error); console.error("Error importing file:", error);
setStatus(`Failed to import 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); console.log("KGStudio file imported successfully:", projectName);
} }
} catch (error) { } catch (error) {
window.alert(`The .kgstudio file is corrupted or invalid: ${error}`); await showAlert(`The .kgstudio file is corrupted or invalid: ${error}`);
throw error; throw error;
} }
}; };
@@ -536,31 +537,31 @@ const Toolbar: React.FC = () => {
}; };
// Prompt to change max bars when clicking on current-time display // Prompt to change max bars when clicking on current-time display
const handleCurrentTimeClick = () => { const handleCurrentTimeClick = async () => {
const MIN_BARS = 16; 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) { if (newMaxBarsStr === null) {
return; // cancelled return; // cancelled
} }
const parsed = parseInt(newMaxBarsStr.trim(), 10); const parsed = parseInt(newMaxBarsStr.trim(), 10);
if (isNaN(parsed)) { if (isNaN(parsed)) {
alert('Invalid input. Please enter a valid number.'); await showAlert('Invalid input. Please enter a valid number.');
return; return;
} }
if (parsed < MIN_BARS) { if (parsed < MIN_BARS) {
alert(`Invalid value. Please enter a number >= ${MIN_BARS}.`); await showAlert(`Invalid value. Please enter a number >= ${MIN_BARS}.`);
return; return;
} }
setMaxBars(parsed); setMaxBars(parsed);
setStatus(`Max bars changed to ${parsed}`); setStatus(`Max bars changed to ${parsed}`);
}; };
const handleBpmClick = () => { const handleBpmClick = async () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("BPM clicked, current BPM:", bpm); 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 // Check if user cancelled
if (newBpmStr === null) { if (newBpmStr === null) {
@@ -572,13 +573,13 @@ const Toolbar: React.FC = () => {
// Check if it's a valid number // Check if it's a valid number
if (isNaN(newBpm)) { if (isNaN(newBpm)) {
alert("Invalid input. Please enter a valid number."); await showAlert("Invalid input. Please enter a valid number.");
return; return;
} }
// Check if it's within valid range // Check if it's within valid range
if (newBpm <= TIME_CONSTANTS.MIN_BPM || newBpm >= TIME_CONSTANTS.MAX_BPM) { 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; return;
} }
@@ -591,13 +592,13 @@ const Toolbar: React.FC = () => {
} }
}; };
const handleTimeSignatureClick = () => { const handleTimeSignatureClick = async () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("Time signature clicked, current:", `${timeSignature.numerator}/${timeSignature.denominator}`); console.log("Time signature clicked, current:", `${timeSignature.numerator}/${timeSignature.denominator}`);
} }
const currentTimeSignatureStr = `${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 // Check if user cancelled
if (newTimeSignatureStr === null) { if (newTimeSignatureStr === null) {
@@ -608,7 +609,7 @@ const Toolbar: React.FC = () => {
const newTimeSignature = parseTimeSignature(newTimeSignatureStr); const newTimeSignature = parseTimeSignature(newTimeSignatureStr);
if (newTimeSignature === null) { if (newTimeSignature === null) {
alert(getTimeSignatureErrorMessage()); await showAlert(getTimeSignatureErrorMessage());
return; return;
} }
@@ -714,17 +715,17 @@ const Toolbar: React.FC = () => {
}; };
// Handle split region button click // Handle split region button click
const handleSplitClick = () => { const handleSplitClick = async () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("Split button clicked"); console.log("Split button clicked");
} }
if (selectedRegionIds.length === 0) { if (selectedRegionIds.length === 0) {
alert("Please select a region to split."); await showAlert("Please select a region to split.");
return; return;
} }
if (selectedRegionIds.length > 1) { if (selectedRegionIds.length > 1) {
alert("Please select exactly one region to split."); await showAlert("Please select exactly one region to split.");
return; return;
} }
@@ -737,7 +738,7 @@ const Toolbar: React.FC = () => {
} }
if (!targetRegion) { if (!targetRegion) {
alert("Selected region not found."); await showAlert("Selected region not found.");
return; return;
} }
@@ -745,7 +746,7 @@ const Toolbar: React.FC = () => {
const regionEnd = regionStart + targetRegion.getLength(); const regionEnd = regionStart + targetRegion.getLength();
if (playheadPosition <= regionStart || playheadPosition >= regionEnd) { 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; return;
} }
@@ -760,13 +761,13 @@ const Toolbar: React.FC = () => {
}; };
// Handle undo button click // Handle undo button click
const handleUndoClick = () => { const handleUndoClick = async () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("Undo button clicked"); console.log("Undo button clicked");
} }
if (!canUndo) { if (!canUndo) {
alert("Nothing to undo"); await showAlert("Nothing to undo");
return; return;
} }
@@ -780,13 +781,13 @@ const Toolbar: React.FC = () => {
}; };
// Handle redo button click // Handle redo button click
const handleRedoClick = () => { const handleRedoClick = async () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("Redo button clicked"); console.log("Redo button clicked");
} }
if (!canRedo) { if (!canRedo) {
alert("Nothing to redo"); await showAlert("Nothing to redo");
return; return;
} }
@@ -830,7 +831,7 @@ const Toolbar: React.FC = () => {
}; };
// Handle Piano button click: open piano roll if closed, targeting active or selected region // Handle Piano button click: open piano roll if closed, targeting active or selected region
const handlePianoButtonClick = () => { const handlePianoButtonClick = async () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log('Piano button clicked'); console.log('Piano button clicked');
} }
@@ -849,6 +850,7 @@ const Toolbar: React.FC = () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log('No active or selected region; piano roll will not open'); 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; return;
} }
+141
View File
@@ -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;
}
+168
View File
@@ -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<void>) | null = null;
let _showConfirmFn: ((message: string, options?: ConfirmOptions) => Promise<boolean>) | null = null;
let _showPromptFn: ((message: string, defaultValue?: string, options?: PromptOptions) => Promise<string | null>) | null = null;
export function showAlert(message: string): Promise<void> {
if (!_showAlertFn) {
window.alert(message);
return Promise.resolve();
}
return _showAlertFn(message);
}
export function showConfirm(message: string, options?: ConfirmOptions): Promise<boolean> {
if (!_showConfirmFn) {
return Promise.resolve(window.confirm(message));
}
return _showConfirmFn(message, options);
}
export function showPrompt(message: string, defaultValue?: string, options?: PromptOptions): Promise<string | null> {
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<DialogInfo | null>(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<void> => {
return new Promise<void>((resolve) => {
resolveRef.current = resolve;
setDialog({ type: 'alert', message });
});
}, []);
const openConfirm = useCallback((message: string, options?: ConfirmOptions): Promise<boolean> => {
return new Promise<boolean>((resolve) => {
resolveRef.current = resolve;
setDialog({ type: 'confirm', message, options });
});
}, []);
const openPrompt = useCallback((message: string, defaultValue?: string, options?: PromptOptions): Promise<string | null> => {
return new Promise<string | null>((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}
<div className="dialog-overlay" onClick={handleOverlayClick}>
<div className="dialog-modal">
<div className="dialog-header">
<h3 className="dialog-title">{title}</h3>
<button
className="dialog-close-btn"
onClick={handleCancel}
aria-label="Close dialog"
>
<FaTimes />
</button>
</div>
<div className="dialog-body">
<p className="dialog-message">{dialog.message}</p>
{isPrompt && (
<input
className="dialog-input"
type="text"
value={inputValue}
placeholder={promptOptions?.placeholder}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleConfirm();
if (e.key === 'Escape') handleCancel();
}}
autoFocus
/>
)}
</div>
<div className="dialog-footer">
{!isAlert && (
<button
className="dialog-btn dialog-btn-cancel"
onClick={handleCancel}
>
{(dialog.options as ConfirmOptions | PromptOptions | undefined)?.cancelLabel ?? 'Cancel'}
</button>
)}
<button
className="dialog-btn dialog-btn-primary"
onClick={handleConfirm}
autoFocus={!isPrompt}
>
{isAlert ? 'OK' : ((dialog.options as ConfirmOptions | PromptOptions | undefined)?.confirmLabel ?? (isPrompt ? 'OK' : 'Yes'))}
</button>
</div>
</div>
</div>
</>
);
};
export default DialogProvider;
+3 -2
View File
@@ -1,6 +1,7 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import './FileImportModal.css'; import './FileImportModal.css';
import { FaTimes } from 'react-icons/fa'; import { FaTimes } from 'react-icons/fa';
import { showAlert } from './DialogProvider';
interface FileImportModalProps { interface FileImportModalProps {
isVisible: boolean; isVisible: boolean;
@@ -41,7 +42,7 @@ const FileImportModal: React.FC<FileImportModalProps> = ({
e.stopPropagation(); e.stopPropagation();
}, []); }, []);
const handleDrop = useCallback((e: React.DragEvent) => { const handleDrop = useCallback(async (e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setIsDragOver(false); setIsDragOver(false);
@@ -56,7 +57,7 @@ const FileImportModal: React.FC<FileImportModalProps> = ({
onFileImport(file); onFileImport(file);
onClose(); onClose();
} else { } 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]); }, [acceptedTypes, onFileImport, onClose]);
+8 -7
View File
@@ -3,6 +3,7 @@ import { FaTimes, FaSortUp, FaSortDown, FaCopy, FaTrash, FaUndo } from 'react-ic
import { KGProjectStorage, type ProjectMeta } from '../../core/io/KGProjectStorage'; import { KGProjectStorage, type ProjectMeta } from '../../core/io/KGProjectStorage';
import { isValidProjectName } from '../../util/projectNameUtil'; import { isValidProjectName } from '../../util/projectNameUtil';
import './OpenProjectModal.css'; import './OpenProjectModal.css';
import { showAlert, showConfirm, showPrompt } from './DialogProvider';
interface OpenProjectModalProps { interface OpenProjectModalProps {
onClose: () => void; onClose: () => void;
@@ -115,12 +116,12 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProj
const handleDuplicate = async (e: React.MouseEvent, projectName: string) => { const handleDuplicate = async (e: React.MouseEvent, projectName: string) => {
e.stopPropagation(); 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; if (!newName || newName.trim() === '') return;
const trimmed = newName.trim(); const trimmed = newName.trim();
if (!isValidProjectName(trimmed)) { 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; return;
} }
@@ -132,7 +133,7 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProj
onClose(); onClose();
} catch (error) { } catch (error) {
console.error('Error duplicating project:', 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<OpenProjectModalProps> = ({ onClose, onOpenProj
await fetchProjects(viewMode); await fetchProjects(viewMode);
} catch (error) { } catch (error) {
console.error('Error deleting project:', 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<OpenProjectModalProps> = ({ onClose, onOpenProj
await fetchProjects(viewMode); await fetchProjects(viewMode);
} catch (error) { } catch (error) {
console.error('Error restoring project:', 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) => { const handlePermanentDelete = async (e: React.MouseEvent, projectName: string) => {
e.stopPropagation(); 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.` `Are you sure you want to permanently delete "${projectName}"?\n\nThis operation cannot be undone.`
); );
if (!confirmed) return; if (!confirmed) return;
@@ -177,7 +178,7 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProj
} }
} catch (error) { } catch (error) {
console.error('Error permanently deleting project:', error); console.error('Error permanently deleting project:', error);
window.alert(`Failed to permanently delete project: ${error}`); await showAlert(`Failed to permanently delete project: ${error}`);
} }
}; };
+3 -1
View File
@@ -2,4 +2,6 @@ export { default as KGDropdown } from './KGDropdown';
export { default as Playhead } from './Playhead'; export { default as Playhead } from './Playhead';
export { default as FileImportModal } from './FileImportModal'; export { default as FileImportModal } from './FileImportModal';
export { default as LoadingOverlay } from './LoadingOverlay'; export { default as LoadingOverlay } from './LoadingOverlay';
export { default as OpenProjectModal } from './OpenProjectModal'; export { default as OpenProjectModal } from './OpenProjectModal';
export { default as DialogProvider, showAlert, showConfirm, showPrompt } from './DialogProvider';
export type { ConfirmOptions, PromptOptions } from './DialogProvider';
+4 -4
View File
@@ -15,6 +15,7 @@ import { ConfigManager } from '../../core/config/ConfigManager';
import { beatsToBar } from '../../util/midiUtil'; import { beatsToBar } from '../../util/midiUtil';
import { UpdateRegionCommand } from '../../core/commands'; import { UpdateRegionCommand } from '../../core/commands';
import { getSuitableChords, noteNameToPitchClass } from '../../util/scaleUtil'; import { getSuitableChords, noteNameToPitchClass } from '../../util/scaleUtil';
import { showAlert, showPrompt } from '../common/DialogProvider';
interface PianoRollProps { interface PianoRollProps {
onClose: () => void; onClose: () => void;
@@ -250,7 +251,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
}, [isDragging, isResizing, dragOffset, position]); }, [isDragging, isResizing, dragOffset, position]);
// Handle title click to rename the region // Handle title click to rename the region
const handleTitleClick = () => { const handleTitleClick = async () => {
// If we were just dragging, don't show the rename dialog // If we were just dragging, don't show the rename dialog
if (wasDraggingRef.current) { if (wasDraggingRef.current) {
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
@@ -262,7 +263,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
if (!activeRegion) return; if (!activeRegion) return;
// Show a prompt to get the new name // 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 the user clicked Cancel or entered an empty string, do nothing
if (!newName || newName.trim() === '' || newName === activeRegion.getName()) return; if (!newName || newName.trim() === '' || newName === activeRegion.getName()) return;
@@ -282,8 +283,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
} catch (error) { } catch (error) {
console.error('Error renaming region:', error); console.error('Error renaming region:', error);
// Optionally show user-friendly error message await showAlert('Failed to rename region. Please try again.');
alert('Failed to rename region. Please try again.');
} }
}; };
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
import { ConfigManager } from '../../../core/config/ConfigManager'; import { ConfigManager } from '../../../core/config/ConfigManager';
import { validateFunctionalChordsJSON } from '../../../util/scaleUtil'; import { validateFunctionalChordsJSON } from '../../../util/scaleUtil';
import { KGCore } from '../../../core/KGCore'; import { KGCore } from '../../../core/KGCore';
import { showAlert } from '../../common/DialogProvider';
const ChordGuideSettings: React.FC = () => { const ChordGuideSettings: React.FC = () => {
const [chordDefinition, setChordDefinition] = useState<string>(''); const [chordDefinition, setChordDefinition] = useState<string>('');
@@ -88,7 +89,7 @@ const ChordGuideSettings: React.FC = () => {
console.log('Loaded default chord template'); console.log('Loaded default chord template');
} catch (error) { } catch (error) {
console.error('Failed to load default template:', 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.');
} }
}; };
+4 -3
View File
@@ -13,6 +13,7 @@ import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface';
import { KGAudioRegion } from '../../core/region/KGAudioRegion'; import { KGAudioRegion } from '../../core/region/KGAudioRegion';
import { generateNewRegionName } from '../../util/miscUtil'; import { generateNewRegionName } from '../../util/miscUtil';
import { KGAudioFileStorage } from '../../core/io/KGAudioFileStorage'; import { KGAudioFileStorage } from '../../core/io/KGAudioFileStorage';
import { showAlert } from '../common/DialogProvider';
import { parseMidiFirstTrackNotes } from '../../util/midiUtil'; import { parseMidiFirstTrackNotes } from '../../util/midiUtil';
import * as Tone from 'tone'; import * as Tone from 'tone';
@@ -52,7 +53,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
const pendingAudioImportRef = useRef<{ barNumber: number; trackIndex: number } | null>(null); const pendingAudioImportRef = useRef<{ barNumber: number; trackIndex: number } | null>(null);
// Utility function to create a region at a specific position // Utility function to create a region at a specific position
const createRegionAtPosition = (e: React.MouseEvent<HTMLDivElement>, trackIndex: number) => { const createRegionAtPosition = async (e: React.MouseEvent<HTMLDivElement>, trackIndex: number) => {
// Get the grid container element // Get the grid container element
const gridContainer = e.currentTarget.closest('.grid-container'); const gridContainer = e.currentTarget.closest('.grid-container');
if (!gridContainer) return; if (!gridContainer) return;
@@ -108,7 +109,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
if (DEBUG_MODE.TRACK_GRID_PANEL) { if (DEBUG_MODE.TRACK_GRID_PANEL) {
console.log(`Cannot create region at bar ${barNumber}: overlaps with existing region`); 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 return; // Don't create the region if it overlaps
} }
@@ -515,7 +516,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
try { try {
if (track.getType() === TrackType.MIDI) { if (track.getType() === TrackType.MIDI) {
if (!dropData.midiUrl) { if (!dropData.midiUrl) {
window.alert( await showAlert(
'This audio clip can only be imported into an audio track.\n' + 'This audio clip can only be imported into an audio track.\n' +
'Please drag it onto an audio track instead.' 'Please drag it onto an audio track instead.'
); );
+6 -5
View File
@@ -12,6 +12,7 @@ import { FLUIDR3_INSTRUMENT_MAP } from '../../constants/generalMidiConstants';
import { DEBUG_MODE } from '../../constants/uiConstants'; import { DEBUG_MODE } from '../../constants/uiConstants';
import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface'; import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface';
import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants'; import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants';
import { showAlert, showConfirm, showPrompt } from '../common/DialogProvider';
interface TrackInfoItemProps { interface TrackInfoItemProps {
track: KGTrack; track: KGTrack;
index: number; index: number;
@@ -97,10 +98,10 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
}, [allTracks, track]); }, [allTracks, track]);
// Handle track name edit within the component // 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 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) { if (newName) {
// Call the parent handler with the new name // Call the parent handler with the new name
onTrackNameEdit(track, newName); onTrackNameEdit(track, newName);
@@ -233,7 +234,7 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
// Handle settings action // Handle settings action
const handleSettingsAction = async (action: string) => { const handleSettingsAction = async (action: string) => {
if (action === 'Delete Track') { 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) { if (confirmed) {
try { try {
if (DEBUG_MODE.TRACK_INFO) { if (DEBUG_MODE.TRACK_INFO) {
@@ -252,7 +253,7 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
setShowSettingsDropdown(false); setShowSettingsDropdown(false);
} catch (error) { } catch (error) {
console.error('Failed to delete track:', 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.');
} }
} }
} }
+8 -5
View File
@@ -4,6 +4,7 @@ import { createRoot } from 'react-dom/client';
import './styles/variables.css'; import './styles/variables.css';
import './index.css'; import './index.css';
import App from './App.tsx'; import App from './App.tsx';
import DialogProvider from './components/common/DialogProvider';
import { KGCore } from './core/KGCore'; import { KGCore } from './core/KGCore';
import { KGAudioInterface } from './core/audio-interface/KGAudioInterface'; import { KGAudioInterface } from './core/audio-interface/KGAudioInterface';
import { KGMidiInput } from './core/midi-input/KGMidiInput'; 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 // Note: Custom messages are no longer supported in modern browsers for security reasons
}); });
root.render( root.render(
<StrictMode> <StrictMode>
<App /> <DialogProvider>
</StrictMode>, <App />
); </DialogProvider>
</StrictMode>,
);
} // end isSecureContext else } // end isSecureContext else
+3 -2
View File
@@ -1,6 +1,7 @@
import { KGProjectStorage } from '../core/io/KGProjectStorage'; import { KGProjectStorage } from '../core/io/KGProjectStorage';
import { KGCore } from '../core/KGCore'; import { KGCore } from '../core/KGCore';
import { RESERVED_PROJECT_NAME } from './projectNameUtil'; import { RESERVED_PROJECT_NAME } from './projectNameUtil';
import { showAlert } from '../components/common/DialogProvider';
/** /**
* Save project utility function. * Save project utility function.
@@ -59,7 +60,7 @@ export const saveProject = async (
return true; return true;
} catch (error) { } catch (error) {
console.error('Error saving renamed project:', 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; return false;
} }
} }
@@ -72,7 +73,7 @@ export const saveProject = async (
return true; return true;
} catch (error) { } catch (error) {
console.error('Error saving project:', 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; return false;
} }
}; };