|
const dispatchDeleteTrack = () => { |
|
// If a track that's not last is being deleted, |
|
if (selectedTrackNumber < tracks.length - 1) { |
|
const nextTracksFirstDurationAtSelectedMeasureNumber = durations.find( |
|
(duration) => |
|
duration.id === |
|
measures.find( |
|
(measure) => |
|
measure.id === |
|
tracks[selectedTrackNumber + 1].measures[selectedMeasureNumber] |
|
).durations[0] |
|
); |
|
|
|
// Select first duration of next track's measure at selectedMeasureNumber |
|
dispatch({ |
|
type: actionTypes.SELECT_DURATION, |
|
durationId: nextTracksFirstDurationAtSelectedMeasureNumber.id, |
|
}); |
|
} |
|
// Otherwise, select first duration of previous track's measure at selectedMeasureNumber |
|
else if (selectedTrackNumber !== 0) { |
|
const previousTracksFirstDurationAtSelectedMeasureNumber = durations.find( |
|
(duration) => |
|
duration.id === |
|
measures.find( |
|
(measure) => |
|
measure.id === |
|
tracks[selectedTrackNumber - 1].measures[selectedMeasureNumber] |
|
).durations[0] |
|
); |
|
|
|
dispatch({ |
|
type: actionTypes.SELECT_TRACK, |
|
trackNumber: selectedTrackNumber - 1, |
|
}); |
|
dispatch({ |
|
type: actionTypes.SELECT_DURATION, |
|
durationId: previousTracksFirstDurationAtSelectedMeasureNumber.id, |
|
}); |
|
} |
|
|
|
dispatch({ |
|
type: actionTypes.DELETE_TRACK, |
|
trackId: tracks[selectedTrackNumber].id, |
|
}); |
|
}; |
|
|
|
const dispatchAddTrack = useCallback( |
|
(trackToAdd) => { |
|
let newTrackId = nanoid(); |
|
// TODO Turn ID array generation into a function |
|
let measureIds = |
|
tracks.length === 0 |
|
? [nanoid()] |
|
: tracks[0].measures.map((measure) => nanoid()); |
|
let durationIds = |
|
tracks.length === 0 |
|
? [nanoid()] |
|
: tracks[0].measures.map((measure) => nanoid()); |
|
|
|
dispatch({ |
|
type: actionTypes.ADD_TRACK, |
|
id: newTrackId, |
|
measures: measureIds, |
|
durationIds: durationIds, |
|
durationLength: selectedDuration?.length, |
|
...trackToAdd, |
|
}); |
|
|
|
return { |
|
newTrackId: newTrackId, |
|
durationIdToSelect: durationIds[selectedMeasureNumber], |
|
}; |
|
}, |
|
[selectedMeasureNumber, selectedDuration, tracks] |
|
); |
|
|
|
const dispatchShortenDuration = useCallback( |
|
(durationId) => { |
|
dispatch({ |
|
type: actionTypes.SET_DURATION_LENGTH, |
|
durationId: durationId, |
|
newLength: selectedDuration?.length / 2, |
|
}); |
|
}, |
|
[selectedDuration] |
|
); |
|
|
|
const dispatchLengthenDuration = useCallback( |
|
(durationId) => { |
|
dispatch({ |
|
type: actionTypes.SET_DURATION_LENGTH, |
|
durationId: durationId, |
|
newLength: selectedDuration?.length * 2, |
|
}); |
|
}, |
|
[selectedDuration] |
|
); |
|
|
|
const dispatchSelectPreviousString = useCallback(() => { |
|
dispatch({ |
|
type: actionTypes.SELECT_STRING, |
|
stringNumber: |
|
selectedStringNumber === 0 |
|
? selectedTrack?.tuning.length - 1 |
|
: selectedStringNumber - 1, |
|
}); |
|
}, [selectedTrack, selectedStringNumber]); |
|
|
|
const dispatchSelectNextString = useCallback(() => { |
|
dispatch({ |
|
type: actionTypes.SELECT_STRING, |
|
stringNumber: (selectedStringNumber + 1) % selectedTrack?.tuning.length, |
|
}); |
|
}, [selectedTrack, selectedStringNumber]); |
|
|
|
const dispatchSelectPreviousDuration = useCallback(() => { |
|
// If currently selected duration is NOT first in the measure, |
|
if (selectedDurationId !== selectedMeasure?.durations[0]) { |
|
// Select the previous duration |
|
dispatch({ |
|
type: actionTypes.SELECT_DURATION, |
|
durationId: |
|
selectedMeasure?.durations[ |
|
selectedMeasure?.durations.findIndex( |
|
(durationId) => durationId === selectedDurationId |
|
) - 1 |
|
], |
|
}); |
|
} else if (selectedMeasureNumber > 0) { |
|
const previousMeasure = measures.find( |
|
(measure) => |
|
measure.id === selectedTrack?.measures[selectedMeasureNumber - 1] |
|
); |
|
const durationIdToSelect = previousMeasure.durations.slice(-1)[0]; |
|
|
|
// Select the last duration of the previous measure |
|
dispatch({ |
|
type: actionTypes.SELECT_MEASURE, |
|
measureNumber: selectedMeasureNumber - 1, |
|
}); |
|
dispatch({ |
|
type: actionTypes.SELECT_DURATION, |
|
durationId: durationIdToSelect, |
|
}); |
|
} |
|
}, [ |
|
measures, |
|
selectedTrack, |
|
selectedMeasureNumber, |
|
selectedMeasure, |
|
selectedDurationId, |
|
]); |
|
|
|
const dispatchSelectNextDuration = useCallback(() => { |
|
let shouldCheckIfMeasureIsLast = false; |
|
|
|
// If there's a note at this duration, |
|
// Or if this duration is a rest, |
|
if (selectedDuration?.notes.length || selectedDuration?.isRest) { |
|
// If this is the last duration, |
|
if (selectedDurationId === selectedMeasure?.durations.slice(-1)[0]) { |
|
// If the measure's total length === maximum, |
|
if (currentBarDuration === currentBarMaximumDuration) { |
|
shouldCheckIfMeasureIsLast = true; |
|
} |
|
// Add a new duration to this measure |
|
else { |
|
let newDurationId = nanoid(); |
|
|
|
dispatch({ |
|
type: actionTypes.ADD_DURATION, |
|
measureId: selectedMeasure?.id, |
|
newDurationId: newDurationId, |
|
length: selectedDuration?.length, |
|
isDotted: selectedDuration?.isDotted, |
|
}); |
|
dispatch({ |
|
type: actionTypes.SELECT_DURATION, |
|
durationId: newDurationId, |
|
}); |
|
} |
|
} |
|
// Select the next duration in this measure |
|
else { |
|
const nextDuration = durations.find( |
|
(duration) => |
|
duration.id === |
|
selectedMeasure?.durations[ |
|
selectedMeasure?.durations.findIndex( |
|
(durationId) => durationId === selectedDurationId |
|
) + 1 |
|
] |
|
); |
|
|
|
dispatch({ |
|
type: actionTypes.SELECT_DURATION, |
|
durationId: nextDuration.id, |
|
}); |
|
} |
|
} else { |
|
shouldCheckIfMeasureIsLast = true; |
|
} |
|
|
|
if (shouldCheckIfMeasureIsLast) { |
|
// If selectedMeasure is last, |
|
// Add a new measure |
|
if (selectedMeasureNumber === selectedTrack?.measures.length - 1) { |
|
// TODO Use parallel arrays like in dispatchAddTrack instead |
|
// Create a mapping from track IDs to new measure IDs |
|
let trackMeasureIds = tracks.reduce((map, track) => { |
|
map[track.id] = { |
|
measureId: nanoid(), |
|
durationId: nanoid(), |
|
}; |
|
return map; |
|
}, {}); |
|
|
|
// TODO Pass in current measure's time signature |
|
dispatch({ |
|
type: actionTypes.ADD_MEASURE, |
|
trackMeasureIds: trackMeasureIds, |
|
durationLength: selectedDuration?.length, |
|
}); |
|
dispatch({ |
|
type: actionTypes.SELECT_MEASURE, |
|
measureNumber: selectedMeasureNumber + 1, |
|
}); |
|
dispatch({ |
|
type: actionTypes.SELECT_DURATION, |
|
durationId: trackMeasureIds[selectedTrack?.id].durationId, |
|
}); |
|
} |
|
// Select the next measure |
|
else { |
|
const nextMeasure = measures.find( |
|
(measure) => |
|
measure.id === selectedTrack?.measures[selectedMeasureNumber + 1] |
|
); |
|
const durationIdToSelect = nextMeasure.durations[0]; |
|
|
|
dispatch({ |
|
type: actionTypes.SELECT_MEASURE, |
|
measureNumber: selectedMeasureNumber + 1, |
|
}); |
|
dispatch({ |
|
type: actionTypes.SELECT_DURATION, |
|
durationId: durationIdToSelect, |
|
}); |
|
} |
|
} |
|
}, [ |
|
tracks, |
|
measures, |
|
durations, |
|
selectedTrack, |
|
selectedMeasureNumber, |
|
selectedMeasure, |
|
selectedDurationId, |
|
selectedDuration, |
|
currentBarDuration, |
|
currentBarMaximumDuration, |
|
]); |
|
|
|
const dispatchDeleteMeasure = useCallback(() => { |
|
if (selectedTrack?.measures.length > 1) { |
|
let newSelectedMeasureNumber; |
|
|
|
if (selectedMeasureNumber > 0) { |
|
newSelectedMeasureNumber = selectedMeasureNumber - 1; |
|
dispatch({ |
|
type: actionTypes.SELECT_MEASURE, |
|
measureNumber: newSelectedMeasureNumber, |
|
}); |
|
} else { |
|
newSelectedMeasureNumber = selectedMeasureNumber + 1; |
|
} |
|
|
|
const durationToSelect = durations.find( |
|
(duration) => |
|
duration.id === |
|
measures.find( |
|
(measure) => |
|
measure.id === selectedTrack?.measures[newSelectedMeasureNumber] |
|
).durations[0] |
|
); |
|
|
|
dispatch({ |
|
type: actionTypes.SELECT_DURATION, |
|
durationId: durationToSelect.id, |
|
}); |
|
|
|
// Even though dispatch runs synchronously, selectedMeasureNumber does not change within this closure, |
|
// so this still deletes the correct measure after SELECT_MEASURE has executed |
|
dispatch({ |
|
type: actionTypes.DELETE_MEASURE, |
|
measureNumber: selectedMeasureNumber, |
|
}); |
|
} |
|
}, [measures, durations, selectedTrack, selectedMeasureNumber]); |
|
|
|
const dispatchDeleteNote = useCallback(() => { |
|
let needToSelectNewDuration = false; |
|
|
|
// If there is a note at this selected duration/string, |
|
if (selectedPositionHasNote) { |
|
// Delete that note |
|
// TODO These lines are horribly inefficient |
|
const selectedNoteId = selectedDuration?.notes.find( |
|
(noteId) => |
|
notes.find((note) => note.id === noteId).string === |
|
selectedStringNumber |
|
); |
|
|
|
dispatch({ type: actionTypes.DELETE_NOTE, noteId: selectedNoteId }); |
|
|
|
// If the deleted note was the last one in the selected duration, |
|
if (selectedDuration?.notes.length === 1) { |
|
// Turn the duration into a rest |
|
dispatch({ |
|
type: actionTypes.ADD_REST, |
|
durationId: selectedDurationId, |
|
}); |
|
} |
|
// Don't select a new duration if we're in the first duration of the document |
|
else if ( |
|
!( |
|
selectedMeasureNumber === 0 && |
|
selectedDurationId === selectedMeasure?.durations[0] |
|
) |
|
) { |
|
needToSelectNewDuration = true; |
|
} |
|
} else { |
|
// If the selected duration is a rest, |
|
if (selectedDuration?.isRest) { |
|
// If this is the only duration in the measure, |
|
if (selectedMeasure?.durations.length === 1) { |
|
// Change the duration to NOT a rest |
|
dispatch({ |
|
type: actionTypes.MARK_DURATION_AS_NOT_REST, |
|
durationId: selectedDurationId, |
|
}); |
|
} else { |
|
// Delete that duration |
|
dispatch({ |
|
type: actionTypes.DELETE_DURATION, |
|
durationId: selectedDurationId, |
|
}); |
|
|
|
needToSelectNewDuration = true; |
|
} |
|
} |
|
} |
|
|
|
if (needToSelectNewDuration) { |
|
let durationIdToSelect; |
|
|
|
// If the selected duration is first in the measure, |
|
if (selectedDurationId === selectedMeasure?.durations[0]) { |
|
// If the first measure of the document is selected, |
|
if (selectedMeasureNumber === 0) { |
|
// Select the next duration of this measure |
|
durationIdToSelect = |
|
selectedMeasure?.durations[ |
|
selectedMeasure?.durations.findIndex( |
|
(durationId) => durationId === selectedDurationId |
|
) + 1 |
|
]; |
|
} else { |
|
// Select the previous measure's last duration |
|
durationIdToSelect = measures[ |
|
selectedMeasureNumber - 1 |
|
].durations.slice(-1)[0]; |
|
|
|
dispatch({ |
|
type: actionTypes.SELECT_MEASURE, |
|
measureNumber: selectedMeasureNumber - 1, |
|
}); |
|
} |
|
} else { |
|
// Select this measure's previous duration |
|
durationIdToSelect = |
|
selectedMeasure?.durations[ |
|
selectedMeasure?.durations.findIndex( |
|
(durationId) => durationId === selectedDurationId |
|
) - 1 |
|
]; |
|
} |
|
|
|
dispatch({ |
|
type: actionTypes.SELECT_DURATION, |
|
durationId: durationIdToSelect, |
|
}); |
|
} |
|
}, [ |
|
measures, |
|
notes, |
|
selectedMeasureNumber, |
|
selectedMeasure, |
|
selectedDurationId, |
|
selectedDuration, |
|
selectedStringNumber, |
|
selectedPositionHasNote, |
|
]); |
|
|
|
const dispatchAddNote = useCallback( |
|
(fretNumber) => { |
|
const fretInputTime = Date.now(); |
|
setLastFretInputTime(fretInputTime); |
|
|
|
// TODO See other comment about "horribly inefficient" |
|
// TODO selectedNote is probably a good variable to have |
|
const currentFretNumber = |
|
notes.find( |
|
(note) => |
|
note.string === selectedStringNumber && |
|
selectedDuration?.notes.includes(note.id) |
|
)?.fret || 0; |
|
const enteredFretNumber = parseInt(fretNumber); |
|
const newFretNumber = currentFretNumber * 10 + enteredFretNumber; |
|
|
|
dispatch({ |
|
type: actionTypes.ADD_NOTE, |
|
durationId: selectedDurationId, |
|
id: nanoid(), |
|
string: selectedStringNumber, |
|
fret: |
|
fretInputTime - lastFretInputTime < SAME_FRET_NUMBER_CUTOFF_TIME && |
|
newFretNumber <= MAXIMUM_FRET_NUMBER |
|
? newFretNumber |
|
: enteredFretNumber, |
|
}); |
|
}, |
|
[ |
|
notes, |
|
selectedDurationId, |
|
selectedDuration, |
|
selectedStringNumber, |
|
lastFretInputTime, |
|
] |
|
); |