mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-09-02 09:34:33 +02:00
Add settings dialog
This commit is contained in:
30
src/App.tsx
30
src/App.tsx
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { SheetMusic } from './components/SheetMusic';
|
import { SheetMusic } from './components/SheetMusic';
|
||||||
import { Controls, type AppSettings } from './components/Controls';
|
import { Controls, type AppSettings } from './components/Controls';
|
||||||
|
import { SettingsModal } from './components/SettingsModal';
|
||||||
import { usePitchDetector } from './hooks/usePitchDetector';
|
import { usePitchDetector } from './hooks/usePitchDetector';
|
||||||
import { useMetronome } from './hooks/useMetronome';
|
import { useMetronome } from './hooks/useMetronome';
|
||||||
import { useAudioPlayer } from './hooks/useAudioPlayer';
|
import { useAudioPlayer } from './hooks/useAudioPlayer';
|
||||||
@@ -17,7 +18,7 @@ import {
|
|||||||
import { INSTRUMENT_DEFINITIONS } from './music/InstrumentConfigs';
|
import { INSTRUMENT_DEFINITIONS } from './music/InstrumentConfigs';
|
||||||
import { Fretboard } from './components/Fretboard';
|
import { Fretboard } from './components/Fretboard';
|
||||||
|
|
||||||
import { Mic, MicOff, SkipForward, HelpCircle, Volume2, X, Guitar } from 'lucide-react';
|
import { Mic, MicOff, SkipForward, HelpCircle, Volume2, X, Guitar, Settings } from 'lucide-react';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import './styles/skip-button.css';
|
import './styles/skip-button.css';
|
||||||
|
|
||||||
@@ -51,13 +52,15 @@ function App() {
|
|||||||
customMinFret: 0,
|
customMinFret: 0,
|
||||||
customMaxFret: 12,
|
customMaxFret: 12,
|
||||||
autoPlaySightReading: false,
|
autoPlaySightReading: false,
|
||||||
autoPlayVolume: 0.5
|
autoPlayVolume: 0.5,
|
||||||
|
disableAnimation: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const [matchStartTime, setMatchStartTime] = useState<number | null>(null);
|
const [matchStartTime, setMatchStartTime] = useState<number | null>(null);
|
||||||
const [feedbackMessage, setFeedbackMessage] = useState<string>("");
|
const [feedbackMessage, setFeedbackMessage] = useState<string>("");
|
||||||
const [revealed, setRevealed] = useState(false);
|
const [revealed, setRevealed] = useState(false);
|
||||||
const [virtualNote, setVirtualNote] = useState<number | null>(null);
|
const [virtualNote, setVirtualNote] = useState<number | null>(null);
|
||||||
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
|
||||||
const currentTuning = TUNINGS[settings.tuningId];
|
const currentTuning = TUNINGS[settings.tuningId];
|
||||||
const currentInstrumentDef = INSTRUMENT_DEFINITIONS[settings.instrument];
|
const currentInstrumentDef = INSTRUMENT_DEFINITIONS[settings.instrument];
|
||||||
@@ -218,12 +221,15 @@ function App() {
|
|||||||
// Immediate transition
|
// Immediate transition
|
||||||
generateNewNote(true); // Keep "Good!" message
|
generateNewNote(true); // Keep "Good!" message
|
||||||
|
|
||||||
// Allow visual feedback to persist for a moment before clearing text (optional, but requested layout changes handle visuals)
|
if (settings.disableAnimation) {
|
||||||
// We rely on CSS overlay fading or just keeping it briefly?
|
setFeedbackMessage("");
|
||||||
// With the new generateNewNote(true), "Good!" stays. We need to clear it eventually.
|
setRevealed(false);
|
||||||
|
} else {
|
||||||
|
// Allow visual feedback to persist for a moment before clearing text
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setFeedbackMessage("");
|
setFeedbackMessage("");
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Rhythm Active AND Auto-Advance
|
// Rhythm Active AND Auto-Advance
|
||||||
@@ -386,6 +392,13 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="icon-button"
|
||||||
|
onClick={() => setIsSettingsOpen(true)}
|
||||||
|
title="Settings"
|
||||||
|
>
|
||||||
|
<Settings size={24} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="icon-button"
|
className="icon-button"
|
||||||
onClick={() => setShowHelp(true)}
|
onClick={() => setShowHelp(true)}
|
||||||
@@ -575,6 +588,13 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<SettingsModal
|
||||||
|
isOpen={isSettingsOpen}
|
||||||
|
onClose={() => setIsSettingsOpen(false)}
|
||||||
|
settings={settings}
|
||||||
|
onUpdateSettings={setSettings}
|
||||||
|
/>
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Settings, Guitar, Music, Gauge, Volume2 } from 'lucide-react';
|
import { Settings, Guitar, Music, Gauge } from 'lucide-react';
|
||||||
import { TUNINGS, INSTRUMENT_TUNINGS } from '../music/Tunings';
|
import { TUNINGS, INSTRUMENT_TUNINGS } from '../music/Tunings';
|
||||||
import { INSTRUMENT_DEFINITIONS } from '../music/InstrumentConfigs';
|
import { INSTRUMENT_DEFINITIONS } from '../music/InstrumentConfigs';
|
||||||
import { getTempoMarking } from '../music/TempoMarkings';
|
import { getTempoMarking } from '../music/TempoMarkings';
|
||||||
@@ -31,6 +31,7 @@ export interface AppSettings {
|
|||||||
customMaxFret?: number;
|
customMaxFret?: number;
|
||||||
autoPlaySightReading?: boolean;
|
autoPlaySightReading?: boolean;
|
||||||
autoPlayVolume?: number;
|
autoPlayVolume?: number;
|
||||||
|
disableAnimation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ControlsProps {
|
interface ControlsProps {
|
||||||
@@ -210,7 +211,7 @@ export const Controls: React.FC<ControlsProps> = ({ settings, onUpdateSettings,
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Section: Tools Grid (3 Columns) */}
|
{/* Bottom Section: Tools Grid (2 Columns now) */}
|
||||||
<div className="tools-grid">
|
<div className="tools-grid">
|
||||||
{/* Tool 1: Tuner */}
|
{/* Tool 1: Tuner */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', border: '1px solid rgba(128,128,128,0.2)', padding: '12px', borderRadius: '8px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', border: '1px solid rgba(128,128,128,0.2)', padding: '12px', borderRadius: '8px' }}>
|
||||||
@@ -356,42 +357,7 @@ export const Controls: React.FC<ControlsProps> = ({ settings, onUpdateSettings,
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tool 3: Auto-play */}
|
{/* Tool 3: Auto-play */}
|
||||||
{/* Tool 3: Auto-play */}
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', border: '1px solid rgba(128,128,128,0.2)', padding: '12px', borderRadius: '8px' }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
||||||
<label className="control-label" style={{ marginBottom: 0, textTransform: 'none' }}>
|
|
||||||
<span>Auto-play</span>
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
className={`switch-button ${settings.gameMode === 'ear_training' || settings.autoPlaySightReading ? 'active' : ''}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (settings.gameMode !== 'ear_training') {
|
|
||||||
onUpdateSettings({ ...settings, autoPlaySightReading: !settings.autoPlaySightReading });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={settings.gameMode === 'ear_training'}
|
|
||||||
style={{ cursor: settings.gameMode === 'ear_training' ? 'not-allowed' : 'pointer', opacity: settings.gameMode === 'ear_training' ? 0.8 : 1 }}
|
|
||||||
title={settings.gameMode === 'ear_training' ? "Forced On in Ear Training" : "Toggle Auto-play"}
|
|
||||||
>
|
|
||||||
<div className="switch-thumb" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(settings.gameMode === 'ear_training' || settings.autoPlaySightReading) && (
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', paddingTop: '8px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
|
|
||||||
<Volume2 size={14} style={{ opacity: 0.7 }} />
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.05"
|
|
||||||
value={settings.autoPlayVolume ?? 0.5}
|
|
||||||
onChange={(e) => onUpdateSettings({ ...settings, autoPlayVolume: parseFloat(e.target.value) })}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
90
src/components/SettingsModal.tsx
Normal file
90
src/components/SettingsModal.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { X, Volume2 } from 'lucide-react';
|
||||||
|
import type { AppSettings } from './Controls';
|
||||||
|
|
||||||
|
interface SettingsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
settings: AppSettings;
|
||||||
|
onUpdateSettings: (s: AppSettings) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose, settings, onUpdateSettings }) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="help-popup-overlay" onClick={onClose}>
|
||||||
|
<div className="help-popup" onClick={e => e.stopPropagation()}>
|
||||||
|
<button className="help-close" onClick={onClose}><X size={20} /></button>
|
||||||
|
<h2 style={{ marginTop: 0, marginBottom: '24px' }}>Settings</h2>
|
||||||
|
|
||||||
|
{/* Animation Settings */}
|
||||||
|
<div className="control-group" style={{ marginBottom: '24px' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||||
|
<label className="control-label" style={{ marginBottom: 0, fontSize: '16px' }}>
|
||||||
|
<span>Disable success animation</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
className={`switch-button ${settings.disableAnimation ? 'active' : ''}`}
|
||||||
|
onClick={() => onUpdateSettings({ ...settings, disableAnimation: !settings.disableAnimation })}
|
||||||
|
title={settings.disableAnimation ? "Enable Animation" : "Disable Animation"}
|
||||||
|
>
|
||||||
|
<div className="switch-thumb" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: '12px', opacity: 0.7, margin: 0 }}>
|
||||||
|
Removes the "Good!" overlay to allow faster playing.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr style={{ borderColor: 'rgba(255,255,255,0.1)', margin: '24px 0' }} />
|
||||||
|
|
||||||
|
{/* Auto Play Settings */}
|
||||||
|
<div className="control-group">
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' }}>
|
||||||
|
<label className="control-label" style={{ marginBottom: 0, fontSize: '16px' }}>
|
||||||
|
<span>Auto-play Note</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
className={`switch-button ${settings.gameMode === 'ear_training' || settings.autoPlaySightReading ? 'active' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (settings.gameMode !== 'ear_training') {
|
||||||
|
onUpdateSettings({ ...settings, autoPlaySightReading: !settings.autoPlaySightReading });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={settings.gameMode === 'ear_training'}
|
||||||
|
style={{
|
||||||
|
cursor: settings.gameMode === 'ear_training' ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: settings.gameMode === 'ear_training' ? 0.8 : 1
|
||||||
|
}}
|
||||||
|
title={settings.gameMode === 'ear_training' ? "Forced On in Ear Training" : "Toggle Auto-play"}
|
||||||
|
>
|
||||||
|
<div className="switch-thumb" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(settings.gameMode === 'ear_training' || settings.autoPlaySightReading) && (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
<Volume2 size={20} style={{ opacity: 0.7 }} />
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.05"
|
||||||
|
value={settings.autoPlayVolume ?? 0.5}
|
||||||
|
onChange={(e) => onUpdateSettings({ ...settings, autoPlayVolume: parseFloat(e.target.value) })}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p style={{ fontSize: '12px', opacity: 0.7, margin: '8px 0 0 0' }}>
|
||||||
|
{settings.gameMode === 'ear_training'
|
||||||
|
? "Automatically plays the note audio (Required for Ear Training)."
|
||||||
|
: "Automatically plays the note audio when a new note appears."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user