feat: replace browser built-in alert/confirm/prompt pop-up with new DialogProvider
This commit is contained in:
+3
-3
@@ -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.'
|
||||
);
|
||||
}
|
||||
|
||||
+39
-37
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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<FileImportModalProps> = ({
|
||||
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<FileImportModalProps> = ({
|
||||
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]);
|
||||
|
||||
@@ -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<OpenProjectModalProps> = ({ 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<OpenProjectModalProps> = ({ 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<OpenProjectModalProps> = ({ 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<OpenProjectModalProps> = ({ 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<OpenProjectModalProps> = ({ 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}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
export { default as OpenProjectModal } from './OpenProjectModal';
|
||||
export { default as DialogProvider, showAlert, showConfirm, showPrompt } from './DialogProvider';
|
||||
export type { ConfirmOptions, PromptOptions } from './DialogProvider';
|
||||
@@ -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<PianoRollProps> = ({
|
||||
}, [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<PianoRollProps> = ({
|
||||
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<PianoRollProps> = ({
|
||||
|
||||
} 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.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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<string>('');
|
||||
@@ -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.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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<TrackGridPanelProps> = ({
|
||||
const pendingAudioImportRef = useRef<{ barNumber: number; trackIndex: number } | null>(null);
|
||||
|
||||
// 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
|
||||
const gridContainer = e.currentTarget.closest('.grid-container');
|
||||
if (!gridContainer) return;
|
||||
@@ -108,7 +109,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
||||
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<TrackGridPanelProps> = ({
|
||||
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.'
|
||||
);
|
||||
|
||||
@@ -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<TrackInfoItemProps> = ({
|
||||
}, [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<TrackInfoItemProps> = ({
|
||||
// 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<TrackInfoItemProps> = ({
|
||||
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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-5
@@ -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(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<DialogProvider>
|
||||
<App />
|
||||
</DialogProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
} // end isSecureContext else
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user