diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index d3cea05..b41a068 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -31,7 +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'; +import { showAlert, showConfirm, showPrompt, showTimeSigPrompt } from './common/DialogProvider'; const Toolbar: React.FC = () => { const { @@ -596,29 +596,21 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log("Time signature clicked, current:", `${timeSignature.numerator}/${timeSignature.denominator}`); } - - const currentTimeSignatureStr = `${timeSignature.numerator}/${timeSignature.denominator}`; - const newTimeSignatureStr = await showPrompt(`Enter new time signature (numerator/denominator):`, currentTimeSignatureStr); - - // Check if user cancelled - if (newTimeSignatureStr === null) { - return; - } - - // Parse and validate time signature - const newTimeSignature = parseTimeSignature(newTimeSignatureStr); - + + const result = await showTimeSigPrompt('Set the time signature:', timeSignature); + if (result === null) return; + + const newTimeSignature = parseTimeSignature(`${result.numerator}/${result.denominator}`); if (newTimeSignature === null) { await showAlert(getTimeSignatureErrorMessage()); return; } - - // Update time signature + setTimeSignature(newTimeSignature); setStatus(`Time signature changed to ${newTimeSignature.numerator}/${newTimeSignature.denominator}`); - + if (DEBUG_MODE.TOOLBAR) { - console.log(`Time signature updated from ${currentTimeSignatureStr} to ${newTimeSignature.numerator}/${newTimeSignature.denominator}`); + console.log(`Time signature updated to ${newTimeSignature.numerator}/${newTimeSignature.denominator}`); } }; diff --git a/src/components/common/DialogProvider.css b/src/components/common/DialogProvider.css index 8cec586..0d7202e 100644 --- a/src/components/common/DialogProvider.css +++ b/src/components/common/DialogProvider.css @@ -101,6 +101,26 @@ border-color: #5a9fd4; } +.dialog-timesig-row { + display: flex; + align-items: center; + gap: 10px; + margin-top: 12px; +} + +.dialog-timesig-input { + width: 72px; + margin-top: 0; + text-align: center; +} + +.dialog-timesig-sep { + color: #b0b0b0; + font-size: 20px; + font-weight: 300; + line-height: 1; +} + .dialog-footer { display: flex; justify-content: flex-end; diff --git a/src/components/common/DialogProvider.tsx b/src/components/common/DialogProvider.tsx index 91804bf..15beb32 100644 --- a/src/components/common/DialogProvider.tsx +++ b/src/components/common/DialogProvider.tsx @@ -13,9 +13,15 @@ export interface PromptOptions { placeholder?: string; } +export interface TimeSigResult { + numerator: number; + denominator: number; +} + 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; +let _showTimeSigFn: ((message: string, defaultValue?: TimeSigResult) => Promise) | null = null; export function showAlert(message: string): Promise { if (!_showAlertFn) { @@ -34,21 +40,35 @@ export function showConfirm(message: string, options?: ConfirmOptions): Promise< export function showPrompt(message: string, defaultValue?: string, options?: PromptOptions): Promise { if (!_showPromptFn) { - return Promise.resolve(window.prompt(message, defaultValue) ); + return Promise.resolve(window.prompt(message, defaultValue)); } return _showPromptFn(message, defaultValue, options); } +export function showTimeSigPrompt(message: string, defaultValue?: TimeSigResult): Promise { + if (!_showTimeSigFn) { + const raw = window.prompt(message, defaultValue ? `${defaultValue.numerator}/${defaultValue.denominator}` : '4/4'); + if (!raw) return Promise.resolve(null); + const [n, d] = raw.split('/').map(Number); + if (!n || !d) return Promise.resolve(null); + return Promise.resolve({ numerator: n, denominator: d }); + } + return _showTimeSigFn(message, defaultValue); +} + interface DialogInfo { - type: 'alert' | 'confirm' | 'prompt'; + type: 'alert' | 'confirm' | 'prompt' | 'timesig'; message: string; options?: ConfirmOptions | PromptOptions; defaultValue?: string; + defaultTimeSig?: TimeSigResult; } const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [dialog, setDialog] = useState(null); const [inputValue, setInputValue] = useState(''); + const [timeSigNumerator, setTimeSigNumerator] = useState(''); + const [timeSigDenominator, setTimeSigDenominator] = useState(''); // eslint-disable-next-line @typescript-eslint/no-explicit-any const resolveRef = useRef<((value: any) => void) | null>(null); @@ -74,21 +94,35 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) = }); }, []); + const openTimeSig = useCallback((message: string, defaultValue?: TimeSigResult): Promise => { + return new Promise((resolve) => { + resolveRef.current = resolve; + setTimeSigNumerator(String(defaultValue?.numerator ?? 4)); + setTimeSigDenominator(String(defaultValue?.denominator ?? 4)); + setDialog({ type: 'timesig', message, defaultTimeSig: defaultValue }); + }); + }, []); + const close = useCallback((value: unknown) => { setDialog(null); setInputValue(''); + setTimeSigNumerator(''); + setTimeSigDenominator(''); if (resolveRef.current) { resolveRef.current(value); resolveRef.current = null; } }, []); + const mouseDownOnOverlay = useRef(false); + const registered = useRef(false); if (!registered.current) { registered.current = true; _showAlertFn = openAlert; _showConfirmFn = openConfirm; _showPromptFn = openPrompt; + _showTimeSigFn = openTimeSig; } if (!dialog) { @@ -97,23 +131,37 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) = const isAlert = dialog.type === 'alert'; const isPrompt = dialog.type === 'prompt'; + const isTimeSig = dialog.type === 'timesig'; const promptOptions = isPrompt ? (dialog.options as PromptOptions | undefined) : undefined; - const title = isAlert ? 'Notice' : isPrompt ? 'Input' : 'Confirm'; + const title = isAlert ? 'Notice' : isTimeSig ? 'Time Signature' : isPrompt ? 'Input' : 'Confirm'; + + const handleOverlayMouseDown = (e: React.MouseEvent) => { + mouseDownOnOverlay.current = e.target === e.currentTarget; + }; const handleOverlayClick = (e: React.MouseEvent) => { - if (e.target === e.currentTarget) { - close(isAlert ? undefined : isPrompt ? null : false); + if (e.target === e.currentTarget && mouseDownOnOverlay.current) { + close(isAlert ? undefined : (isPrompt || isTimeSig) ? null : false); } }; - const handleCancel = () => close(isAlert ? undefined : isPrompt ? null : false); - const handleConfirm = () => close(isAlert ? undefined : isPrompt ? inputValue : true); + const handleCancel = () => close(isAlert ? undefined : (isPrompt || isTimeSig) ? null : false); + + const handleConfirm = () => { + if (isAlert) { close(undefined); return; } + if (isPrompt) { close(inputValue); return; } + if (isTimeSig) { + close({ numerator: Number(timeSigNumerator), denominator: Number(timeSigDenominator) }); + return; + } + close(true); + }; return ( <> {children} -
+

{title}

@@ -141,6 +189,34 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) = autoFocus /> )} + {isTimeSig && ( +
+ setTimeSigNumerator(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleConfirm(); + if (e.key === 'Escape') handleCancel(); + }} + autoFocus + /> + / + setTimeSigDenominator(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleConfirm(); + if (e.key === 'Escape') handleCancel(); + }} + /> +
+ )}
{!isAlert && ( @@ -154,9 +230,9 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
diff --git a/src/components/common/index.ts b/src/components/common/index.ts index b7a19bd..4dd4446 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -3,5 +3,5 @@ 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 DialogProvider, showAlert, showConfirm, showPrompt } from './DialogProvider'; -export type { ConfirmOptions, PromptOptions } from './DialogProvider'; \ No newline at end of file +export { default as DialogProvider, showAlert, showConfirm, showPrompt, showTimeSigPrompt } from './DialogProvider'; +export type { ConfirmOptions, PromptOptions, TimeSigResult } from './DialogProvider'; \ No newline at end of file