diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/form.ts | 191 | ||||
-rw-r--r-- | src/view/options.ts | 70 | ||||
-rw-r--r-- | src/view/play.ts | 10 |
3 files changed, 200 insertions, 71 deletions
diff --git a/src/view/form.ts b/src/view/form.ts index 5547e0c..e499b05 100644 --- a/src/view/form.ts +++ b/src/view/form.ts @@ -1,5 +1,6 @@ -import { h, withVar, Html, Rx } from 'lib/rx' +import { h, withState, Html, Rx, pure, RxAble } from 'lib/rx' import * as Options from 'view/options' +import * as Chord from 'chord' interface Params { options: Options.Model @@ -7,7 +8,7 @@ interface Params { } export function view({ options, onSubmit }: Params): Html { - return withVar(options, (opts, updateOptions) => + return withState(options, opts => h('form', { className: 'g-Form', onsubmit: opts.map(o => @@ -17,58 +18,71 @@ export function view({ options, onSubmit }: Params): Html { } ) }, - chordCheckbox({ - label: '', - checked: opts.map(o => o.major), - onCheck: (checked => updateOptions(o => { - o.major = checked - return o - })) - }), - chordCheckbox({ - label: '-', - checked: opts.map(o => o.minor), - onCheck: (checked => updateOptions(o => { - o.minor = checked - return o - })) - }), - chordCheckbox({ - label: '7', - checked: opts.map(o => o.seventh), - onCheck: (checked => updateOptions(o => { - o.seventh = checked - return o - })) - }), - chordCheckbox({ - label: '-7', - checked: opts.map(o => o.minorSeventh), - onCheck: (checked => updateOptions(o => { - o.minorSeventh = checked - return o - })) - }), - chordCheckbox({ - label: '7', - checked: opts.map(o => o.majorSeventh), - onCheck: (checked => updateOptions(o => { - o.majorSeventh = checked - return o - })) - }), - numberInput({ - label: 'BPM', + h('table', + { className: 'g-ChordsTable' }, + chordsGroup({ + title: 'Major', + chords: opts.map(o => o.major), + updateChords: (chords: Chord.Chords) => { + opts.update(o => { + o.major = chords + return o + }) + } + }), + chordsGroup({ + title: 'Minor', + chords: opts.map(o => o.minor), + updateChords: (chords: Chord.Chords) => { + opts.update(o => { + o.minor = chords + return o + }) + } + }), + chordsGroup({ + title: '7th', + chords: opts.map(o => o.seventh), + updateChords: (chords: Chord.Chords) => { + opts.update(o => { + o.seventh = chords + return o + }) + } + }), + chordsGroup({ + title: 'Minor 7th', + chords: opts.map(o => o.minorSeventh), + updateChords: (chords: Chord.Chords) => { + opts.update(o => { + o.minorSeventh = chords + return o + }) + } + }), + chordsGroup({ + title: 'Major 7th', + chords: opts.map(o => o.majorSeventh), + updateChords: (chords: Chord.Chords) => { + opts.update(o => { + o.majorSeventh = chords + return o + }) + } + }) + ), + numberInput({ + label: 'BPM', value: opts.map(o => o.bpm.toString()), - onChange: (n => updateOptions(o => { + onChange: (n => opts.update(o => { o.bpm = n return o })) }), - numberInput({ - label: 'Beats per Chord', + numberInput({ + label: 'Beats per Chord', value: opts.map(o => o.beatsPerChord.toString()), - onChange: (n => updateOptions(o => { + onChange: (n => opts.update(o => { o.beatsPerChord = n return o })) @@ -78,10 +92,85 @@ export function view({ options, onSubmit }: Params): Html { ) } +interface ChordsGroupParams { + title: string, + chords: Rx<Chord.Chords>, + updateChords: (chords: Chord.Chords) => void +} + +function chordsGroup({ title, chords, updateChords }: ChordsGroupParams): Html { + const line = + (lineChords: Array<Chord.Chord>, label: (chord: Chord.Chord) => string) => + h('div', + { className: 'g-ChordsLine' }, + chordCheckbox({ + checked: chords.map(cs => lineChords.every(c => cs.has(c))), + onCheck: chords.map(cs => + (checked: boolean) => { + if (checked) { + lineChords.forEach(c => cs.add(c)) + } else { + lineChords.forEach(c => cs.delete(c)) + } + updateChords(cs) + } + ) + }), + lineChords.map(chord => + chordCheckbox({ + label: label(chord), + checked: chords.map(cs => cs.has(chord)), + onCheck: chords.map(cs => + (checked: boolean) => { + if (checked) { + cs.add(chord) + } else { + cs.delete(chord) + } + updateChords(cs) + } + ) + }) + ) + ) + + return h('tr', + h('td', + chordCheckbox({ + label: title, + checked: chords.map(cs => + Chord.sharps.every(c => cs.has(c)) + && Chord.plains.every(c => cs.has(c)) + && Chord.bemols.every(c => cs.has(c)) + ), + onCheck: chords.map(cs => + (checked: boolean) => { + if (checked) { + Chord.sharps.forEach(c => cs.add(c)) + Chord.plains.forEach(c => cs.add(c)) + Chord.bemols.forEach(c => cs.add(c)) + } else { + Chord.sharps.forEach(c => cs.delete(c)) + Chord.plains.forEach(c => cs.delete(c)) + Chord.bemols.forEach(c => cs.delete(c)) + } + updateChords(cs) + } + ) + }) + ), + h('td', + line(Chord.sharps, () => '♯'), + line(Chord.plains, chord => chord), + line(Chord.bemols, () => '♭') + ) + ) +} + interface ChordCheckboxParams { - label: string + label?: string checked: Rx<boolean> - onCheck: (checked: boolean) => void + onCheck: Rx<(checked: boolean) => void> } function chordCheckbox({ label, checked, onCheck }: ChordCheckboxParams): Html { @@ -90,7 +179,7 @@ function chordCheckbox({ label, checked, onCheck }: ChordCheckboxParams): Html { h('input', { type: 'checkbox', checked, - onchange: (event: Event) => onCheck((event.target as HTMLInputElement).checked) + onchange: onCheck.map(f => (event: Event) => f((event.target as HTMLInputElement).checked)) } ), label diff --git a/src/view/options.ts b/src/view/options.ts index 31fd631..0782fe4 100644 --- a/src/view/options.ts +++ b/src/view/options.ts @@ -1,30 +1,70 @@ +import * as Chord from 'chord' + +// Model version: upgrade whenever the model is updated. Options are loaded +// only if versions match. +const version = '2' + export interface Model { - major: boolean - minor: boolean - seventh: boolean - minorSeventh: boolean - majorSeventh: boolean + major: Chord.Chords + minor: Chord.Chords + seventh: Chord.Chords + minorSeventh: Chord.Chords + majorSeventh: Chord.Chords bpm: number beatsPerChord: number } let init: Model = { - major: true, - minor: false, - seventh: false, - minorSeventh: false, - majorSeventh: false, + major: new Set(Chord.plains), + minor: new Set(), + seventh: new Set(), + minorSeventh: new Set(), + majorSeventh: new Set(), bpm: 90, beatsPerChord: 4 } -let key: string = 'options' - export function load(): Model { - let str = localStorage[key] - return str && JSON.parse(str) || init + let storedVersion = localStorage['version'] + + if (storedVersion === undefined || storedVersion !== version) { + return init + } else { + let str = localStorage['options'] + return str && fromString(str) || init + } } export function save(options: Model): void { - localStorage[key] = JSON.stringify(options) + localStorage['version'] = version + localStorage['options'] = toString(options) +} + +function toString(o: Model): string { + return JSON.stringify({ + major: [...o.major], + minor: [...o.minor], + seventh: [...o.seventh], + minorSeventh: [...o.minorSeventh], + majorSeventh: [...o.majorSeventh], + bpm: o.bpm, + beatsPerChord: o.beatsPerChord + }) +} + +function fromString(str: string): Model | undefined { + const o = JSON.parse(str) + if (o === undefined) { + return undefined + } else { + return { + major: new Set(o.major), + minor: new Set(o.minor), + seventh: new Set(o.seventh), + minorSeventh: new Set(o.minorSeventh), + majorSeventh: new Set(o.majorSeventh), + bpm: o.bpm, + beatsPerChord: o.beatsPerChord + } + } } diff --git a/src/view/play.ts b/src/view/play.ts index b85e505..4abc4ec 100644 --- a/src/view/play.ts +++ b/src/view/play.ts @@ -1,4 +1,4 @@ -import { h, withVar, Html } from 'lib/rx' +import { h, withState, Html } from 'lib/rx' import * as Options from 'view/options' import * as Chord from 'chord' @@ -9,19 +9,19 @@ interface ViewParams { export function view({ options }: ViewParams): Html { const initChords: Array<[string, string]> = [['', ''], Chord.generate(options), Chord.generate(options)] - return withVar(initChords, (chords, updateChords) => - withVar(undefined, (beat, updateBeat) => { + return withState(initChords, chords => + withState(undefined, beat => { let chordBeat = 1 const interval = setInterval(() => { if (chordBeat === options.beatsPerChord) { - updateChords(xs => { + chords.update(xs => { xs.shift() xs.push(Chord.generate(options)) return xs }) chordBeat = 1 } else { - updateBeat(_ => undefined) + beat.update(_ => undefined) chordBeat += 1 } }, 60 / options.bpm * 1000) |