diff --git a/packages/app-web/src/components/molecules/timer/Timer/index.tsx b/packages/app-web/src/components/molecules/timer/Timer/index.tsx new file mode 100644 index 0000000..a582f1f --- /dev/null +++ b/packages/app-web/src/components/molecules/timer/Timer/index.tsx @@ -0,0 +1,32 @@ +import {useEffect} from 'react'; + +const Timer = ({ + interval, + onTick, + resume = false, + start, +}) => { + useEffect(() => { + let intervalHandler + if (resume) { + intervalHandler = setInterval(() => { + onTick(Date.now()) + }, interval) + } + return () => { + if (intervalHandler) { + clearInterval(intervalHandler) + } + } + }, [interval, resume]) + + useEffect(() => { + onTick(start) + }, [start]) + + return ( + <> + ) +} + +export default Timer diff --git a/packages/app-web/src/components/organisms/forms/CreateRingtone/index.tsx b/packages/app-web/src/components/organisms/forms/CreateRingtone/index.tsx index 3213707..386a5f2 100644 --- a/packages/app-web/src/components/organisms/forms/CreateRingtone/index.tsx +++ b/packages/app-web/src/components/organisms/forms/CreateRingtone/index.tsx @@ -1,32 +1,69 @@ import {CSSProperties, FC, FormEventHandler, useEffect, useRef, useState} from 'react'; import {models} from '@tonality/library-common'; +import { LeftSidebarWithMenu } from '@theoryofnekomata/viewfinder' import styled from 'styled-components'; import MusicalKeyboard from '@theoryofnekomata/react-musical-keyboard'; -import getFormValues from '@theoryofnekomata/formxtra'; import TextInput from '../../../molecules/forms/TextInput'; import TextArea from '../../../molecules/forms/TextArea'; import ActionButton from '../../../molecules/forms/ActionButton'; -import {convertToRawDuration, formatKey} from '../../../../utils/format/keyData'; import ToggleButton from '../../../molecules/forms/ToggleButton'; import NumericInput from '../../../molecules/forms/NumericInput'; -import WaveOscillator from '../../../../utils/sound/WaveOscillator'; -import {parseSong} from '../../../../utils/format/song'; +import {ALLOWED_DURATIONS, toRawDuration} from '@tonality/library-song-utils'; +import Timer from '../../../molecules/timer/Timer'; -const Form = styled('form')({ +const KeyboardContainer = styled(LeftSidebarWithMenu.ContentContainer)({ + '--color-natural-key': 'var(--color-bg, white)', + '--color-accidental-key': 'var(--color-fg, black)', + maxWidth: 'none', + marginTop: '1rem', + marginBottom: '1rem', + padding: 0, + overflow: 'auto', + '@media (min-width: 1080px)': { + maxWidth: 'calc(var(--config-base-width,360) * 2)', + padding: '0 1rem', + }, +}) + +const KeyboardSizer = styled('div')({ + width: 1500, + '@media (min-width: 1080px)': { + width: '100%', + }, +}) + +const Form = styled('form')({}); + +const FormContents = styled('div')({ display: 'grid', gap: '1rem', -}); +}) + +const Primary = styled('div')({ + display: 'grid', + gridTemplateColumns: 'auto 6rem', + gap: '1rem', +}) const Toolbar = styled('div')({ display: 'grid', - gridTemplateColumns: '6fr 1fr 1fr 1fr', gap: '1rem', fontSize: '1.5rem', + '@media (min-width: 720px)': { + gridTemplateColumns: '2fr 1fr', + }, }) const DurationSelector = styled('div')({ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', + gap: '0.25rem', +}) + +const OtherTools = styled('div')({ + display: 'grid', + gridTemplateColumns: 'repeat(3, 1fr)', + gap: '1rem', }) const NOTE_GLYPHS = { @@ -52,6 +89,13 @@ type Props = { action?: string, labels: Record, defaultValues?: Partial, + updateTempo: ({ dataRef, setTempo, }) => (...args: unknown[]) => unknown, + updateView: ({ setNoteGlyph, setRestGlyph, noteGlyphs, restGlyphs, }) => (...args: unknown[]) => unknown, + addRest: ({ formRef, dataRef, }) => (...args: unknown[]) => unknown, + addNote: ({ dataRef, formRef, }) => (...args: unknown[]) => unknown, + togglePlayback: ({ formRef, setPlayTimestamp, setPlaying, }) => (...args: unknown[]) => unknown, + updateSong: ({ formRef, }) => (...args: unknown[]) => unknown, + play: ({ dataRef, playing, setPlaying, setNotes, playTimestamp, currentTime, }) => unknown, } const CreateRingtoneForm: FC = ({ @@ -59,318 +103,183 @@ const CreateRingtoneForm: FC = ({ action, labels, defaultValues = {}, + updateTempo, + updateView, + addRest, + addNote, + togglePlayback, + updateSong, + play, }) => { const [hydrated, setHydrated] = useState(false); + const [tempo, setTempo] = useState(defaultValues.tempo || 120); + const [currentTime, setCurrentTime] = useState(0) const [noteGlyph, setNoteGlyph] = useState(NOTE_GLYPHS[4]); const [restGlyph, setRestGlyph] = useState(REST_GLYPHS[4]); const [playTimestamp, setPlayTimestamp] = useState(0) const [playing, setPlaying] = useState(false) - const [currentTime, setCurrentTime] = useState(0) + const [notes, setNotes] = useState([]) const dataRef = useRef(null); const formRef = useRef(null); - const songRef = useRef({ - notes: [], - duration: 0, - index: 0, - }); - - const addNote = (args) => { - if (!(args.length > 0 && dataRef.current && formRef.current)) { - return; - } - const values = getFormValues(formRef.current); - const isDotted = Boolean(values['dotted']) - const duration = Number(values['duration']) as (1 | 2 | 4 | 8 | 16 | 32) - const tempo = Number(values['tempo']) - const rawDuration = convertToRawDuration(duration, tempo) - const formattedKeyData = formatKey({ - key: args[0].key, - dotted: isDotted, - duration, - }); - const newSongData = `${dataRef.current.value} ${formattedKeyData}`.trim() - dataRef.current.value = newSongData; - songRef.current = parseSong(newSongData, Number(values['tempo'])) - const oscillator = new WaveOscillator() - oscillator.start(args[0].key) - setTimeout(() => { - oscillator.stop() - }, rawDuration) - }; - - const addRest = () => { - if (!(formRef.current && dataRef.current)) { - return; - } - const values = getFormValues(formRef.current); - const formattedKeyData = formatKey({ - dotted: Boolean(values['dotted']), - duration: Number(values['duration']) as (1 | 2 | 4 | 8 | 16 | 32), - }); - const newSongData = `${dataRef.current.value} ${formattedKeyData}`.trim() - dataRef.current.value = newSongData; - songRef.current = parseSong(newSongData, Number(values['tempo'])) - }; - - const updateTempo = (e) => { - if (!isNaN(e.target.valueAsNumber) && e.target.valueAsNumber > 0) { - songRef.current = parseSong(dataRef.current.value, e.target.valueAsNumber) - } - } - const togglePlayback = () => { - if (!formRef.current) { - return - } - setPlayTimestamp(Date.now()) - setPlaying(p => { - const newValue = !p - songRef.current.notes.forEach(n => { - if (!isNaN(n.key)) { - n.osc = n.osc || new WaveOscillator() - if (!newValue && !n.stopped) { - n.osc.stop() - } - } - n.played = newValue ? false : n.played - n.stopped = newValue ? false : n.stopped - }) - return newValue - }) + const handleTick = (now) => { + setCurrentTime(now) } - const updateSong = () => { - const values = getFormValues(formRef.current); - songRef.current = parseSong(values['data'], Number(values['tempo'])) - } - - const updateView = (e) => { - const duration = Number(e.target.value); - setNoteGlyph(NOTE_GLYPHS[duration]); - setRestGlyph(REST_GLYPHS[duration]); - }; - useEffect(() => { - let interval = setInterval(() => { - setCurrentTime(Date.now()) - }, 50) - - return () => { - clearInterval(interval) + if (typeof play === 'function') { + play({dataRef, playing, currentTime, setNotes, setPlaying, playTimestamp,}) } - }, []) - - useEffect(() => { - if (!playing) { - return - } - const cursor = currentTime - playTimestamp - if (cursor >= songRef.current.duration) { - setPlaying(false) - dataRef.current.setSelectionRange(null, null) - } - - songRef.current.notes.forEach((n) => { - const toPlay = !n.played && n.start < cursor - const toStop = !n.stopped && n.end < cursor - if (toPlay) { - if (!isNaN(n.key) && n.osc) { - n.osc.start(n.key) - } - dataRef.current.select() - dataRef.current.setSelectionRange(n.startIndex, n.endIndex) - n.played = true - } - - if (toStop) { - if (!isNaN(n.key) && n.osc) { - n.osc.stop() - } - n.stopped = true - } - }) - }, [currentTime, playTimestamp, playing]) + }, [playTimestamp, playing, currentTime, setNotes, setPlaying, dataRef]) useEffect(() => { - setHydrated(true); - }, []); + setHydrated(true) + }, []) // @ts-ignore return ( -
- { - typeof (defaultValues.id as unknown) === 'string' - && ( - - ) - } - - - - { - hydrated - && ( - - - - 𝅝 - - - 𝅗𝅥 - - - ♩ - - - ♪ - - - 𝅘𝅥𝅯 - - + + + + + { + typeof (defaultValues.id as unknown) === 'string' + && ( + + ) + } + + + + - 𝅘𝅥𝅰 - - - - {noteGlyph + ' + .'} - - - {restGlyph} - - - {playing ? '⏹' : '▶️'} - - - ) - } -