Tidy app
Now that I have a deployed an MVP of my app, I am tidying things up to support the next phase of development. TL;DR: - Moved application Model-related code into State module - Moved each View into its own module - Deleted unused ChordInspector component - Deleted unused Msg's, {Increase,Decrease}Tempo - Deleted misc unused code
This commit is contained in:
parent
ddbd7e2ef5
commit
441fe3e32e
7 changed files with 498 additions and 538 deletions
|
@ -1,15 +0,0 @@
|
||||||
module ChordInspector exposing (render)
|
|
||||||
|
|
||||||
import Html exposing (..)
|
|
||||||
import NoteInspector
|
|
||||||
import Theory
|
|
||||||
|
|
||||||
|
|
||||||
render : Theory.Chord -> Html a
|
|
||||||
render chord =
|
|
||||||
case Theory.notesForChord chord of
|
|
||||||
Nothing ->
|
|
||||||
p [] [ text "Cannot retrieve the notes for the chord." ]
|
|
||||||
|
|
||||||
Just notes ->
|
|
||||||
NoteInspector.render notes
|
|
|
@ -2,544 +2,40 @@ module Main exposing (main)
|
||||||
|
|
||||||
import Browser
|
import Browser
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Misc
|
||||||
import Html.Events exposing (..)
|
import Overview
|
||||||
import Icon
|
import Practice
|
||||||
import Piano
|
import Preferences
|
||||||
import Random
|
import State
|
||||||
import Random.List
|
|
||||||
import Tempo
|
|
||||||
import Theory
|
|
||||||
import Time exposing (..)
|
import Time exposing (..)
|
||||||
import UI
|
|
||||||
|
|
||||||
|
|
||||||
type alias Model =
|
subscriptions : State.Model -> Sub State.Msg
|
||||||
{ whitelistedChords : List Theory.Chord
|
subscriptions model =
|
||||||
, whitelistedChordTypes : List Theory.ChordType
|
if model.isPaused then
|
||||||
, whitelistedInversions : List Theory.ChordInversion
|
|
||||||
, whitelistedPitchClasses : List Theory.PitchClass
|
|
||||||
, whitelistedKeys : List Theory.Key
|
|
||||||
, selectedChord : Maybe Theory.Chord
|
|
||||||
, isPaused : Bool
|
|
||||||
, tempo : Int
|
|
||||||
, firstNote : Theory.Note
|
|
||||||
, lastNote : Theory.Note
|
|
||||||
, practiceMode : PracticeMode
|
|
||||||
, view : View
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type View
|
|
||||||
= Preferences
|
|
||||||
| Practice
|
|
||||||
|
|
||||||
|
|
||||||
{-| Control the type of practice you'd like.
|
|
||||||
-}
|
|
||||||
type PracticeMode
|
|
||||||
= KeyMode
|
|
||||||
| FineTuneMode
|
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
|
||||||
= NextChord
|
|
||||||
| NewChord Theory.Chord
|
|
||||||
| Play
|
|
||||||
| Pause
|
|
||||||
| IncreaseTempo
|
|
||||||
| DecreaseTempo
|
|
||||||
| SetTempo String
|
|
||||||
| ToggleInversion Theory.ChordInversion
|
|
||||||
| ToggleChordType Theory.ChordType
|
|
||||||
| TogglePitchClass Theory.PitchClass
|
|
||||||
| ToggleKey Theory.Key
|
|
||||||
| DoNothing
|
|
||||||
| SetPracticeMode PracticeMode
|
|
||||||
| SelectAllKeys
|
|
||||||
| DeselectAllKeys
|
|
||||||
| SetView View
|
|
||||||
|
|
||||||
|
|
||||||
{-| The amount by which we increase or decrease tempo.
|
|
||||||
-}
|
|
||||||
tempoStep : Int
|
|
||||||
tempoStep =
|
|
||||||
5
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return the number of milliseconds that elapse during an interval in a
|
|
||||||
`target` bpm.
|
|
||||||
-}
|
|
||||||
bpmToMilliseconds : Int -> Int
|
|
||||||
bpmToMilliseconds target =
|
|
||||||
let
|
|
||||||
msPerMinute =
|
|
||||||
1000 * 60
|
|
||||||
in
|
|
||||||
round (toFloat msPerMinute / toFloat target)
|
|
||||||
|
|
||||||
|
|
||||||
{-| The initial state for the application.
|
|
||||||
-}
|
|
||||||
init : Model
|
|
||||||
init =
|
|
||||||
let
|
|
||||||
( firstNote, lastNote ) =
|
|
||||||
( Theory.C3, Theory.C6 )
|
|
||||||
|
|
||||||
inversions =
|
|
||||||
Theory.allInversions
|
|
||||||
|
|
||||||
chordTypes =
|
|
||||||
Theory.allChordTypes
|
|
||||||
|
|
||||||
pitchClasses =
|
|
||||||
Theory.allPitchClasses
|
|
||||||
|
|
||||||
keys =
|
|
||||||
[ { pitchClass = Theory.C, mode = Theory.MajorMode } ]
|
|
||||||
|
|
||||||
practiceMode =
|
|
||||||
KeyMode
|
|
||||||
in
|
|
||||||
{ practiceMode = practiceMode
|
|
||||||
, whitelistedChords =
|
|
||||||
case practiceMode of
|
|
||||||
KeyMode ->
|
|
||||||
keys |> List.concatMap Theory.chordsForKey
|
|
||||||
|
|
||||||
FineTuneMode ->
|
|
||||||
Theory.allChords
|
|
||||||
{ start = firstNote
|
|
||||||
, end = lastNote
|
|
||||||
, inversions = inversions
|
|
||||||
, chordTypes = chordTypes
|
|
||||||
, pitchClasses = pitchClasses
|
|
||||||
}
|
|
||||||
, whitelistedChordTypes = chordTypes
|
|
||||||
, whitelistedInversions = inversions
|
|
||||||
, whitelistedPitchClasses = pitchClasses
|
|
||||||
, whitelistedKeys = keys
|
|
||||||
, selectedChord = Nothing
|
|
||||||
, isPaused = True
|
|
||||||
, tempo = 20
|
|
||||||
, firstNote = firstNote
|
|
||||||
, lastNote = lastNote
|
|
||||||
, view = Preferences
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
subscriptions : Model -> Sub Msg
|
|
||||||
subscriptions { isPaused, tempo } =
|
|
||||||
if isPaused then
|
|
||||||
Sub.none
|
Sub.none
|
||||||
|
|
||||||
else
|
else
|
||||||
Time.every (tempo |> bpmToMilliseconds |> toFloat) (\_ -> NextChord)
|
Time.every (model.tempo |> Misc.bpmToMilliseconds |> toFloat) (\_ -> State.NextChord)
|
||||||
|
|
||||||
|
|
||||||
{-| Now that we have state, we need a function to change the state.
|
view : State.Model -> Html State.Msg
|
||||||
-}
|
|
||||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
|
||||||
update msg model =
|
|
||||||
case msg of
|
|
||||||
DoNothing ->
|
|
||||||
( model, Cmd.none )
|
|
||||||
|
|
||||||
SetPracticeMode practiceMode ->
|
|
||||||
( { model
|
|
||||||
| practiceMode = practiceMode
|
|
||||||
, isPaused = True
|
|
||||||
}
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
SetView x ->
|
|
||||||
( { model
|
|
||||||
| view = x
|
|
||||||
, isPaused = True
|
|
||||||
}
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
SelectAllKeys ->
|
|
||||||
( { model
|
|
||||||
| whitelistedKeys = Theory.allKeys
|
|
||||||
, whitelistedChords =
|
|
||||||
Theory.allKeys |> List.concatMap Theory.chordsForKey
|
|
||||||
}
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
DeselectAllKeys ->
|
|
||||||
( { model
|
|
||||||
| whitelistedKeys = []
|
|
||||||
, whitelistedChords = []
|
|
||||||
}
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
NewChord chord ->
|
|
||||||
( { model | selectedChord = Just chord }
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
NextChord ->
|
|
||||||
( model
|
|
||||||
, Random.generate
|
|
||||||
(\x ->
|
|
||||||
case x of
|
|
||||||
( Just chord, _ ) ->
|
|
||||||
NewChord chord
|
|
||||||
|
|
||||||
( Nothing, _ ) ->
|
|
||||||
DoNothing
|
|
||||||
)
|
|
||||||
(Random.List.choose model.whitelistedChords)
|
|
||||||
)
|
|
||||||
|
|
||||||
Play ->
|
|
||||||
( { model | isPaused = False }
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
Pause ->
|
|
||||||
( { model | isPaused = True }
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
IncreaseTempo ->
|
|
||||||
( { model | tempo = model.tempo + tempoStep }
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
DecreaseTempo ->
|
|
||||||
( { model | tempo = model.tempo - tempoStep }
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
ToggleChordType chordType ->
|
|
||||||
let
|
|
||||||
chordTypes =
|
|
||||||
if List.member chordType model.whitelistedChordTypes then
|
|
||||||
List.filter ((/=) chordType) model.whitelistedChordTypes
|
|
||||||
|
|
||||||
else
|
|
||||||
chordType :: model.whitelistedChordTypes
|
|
||||||
in
|
|
||||||
( { model
|
|
||||||
| whitelistedChordTypes = chordTypes
|
|
||||||
, whitelistedChords =
|
|
||||||
Theory.allChords
|
|
||||||
{ start = model.firstNote
|
|
||||||
, end = model.lastNote
|
|
||||||
, inversions = model.whitelistedInversions
|
|
||||||
, chordTypes = chordTypes
|
|
||||||
, pitchClasses = model.whitelistedPitchClasses
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
ToggleInversion inversion ->
|
|
||||||
let
|
|
||||||
inversions =
|
|
||||||
if List.member inversion model.whitelistedInversions then
|
|
||||||
List.filter ((/=) inversion) model.whitelistedInversions
|
|
||||||
|
|
||||||
else
|
|
||||||
inversion :: model.whitelistedInversions
|
|
||||||
in
|
|
||||||
( { model
|
|
||||||
| whitelistedInversions = inversions
|
|
||||||
, whitelistedChords =
|
|
||||||
Theory.allChords
|
|
||||||
{ start = model.firstNote
|
|
||||||
, end = model.lastNote
|
|
||||||
, inversions = inversions
|
|
||||||
, chordTypes = model.whitelistedChordTypes
|
|
||||||
, pitchClasses = model.whitelistedPitchClasses
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
TogglePitchClass pitchClass ->
|
|
||||||
let
|
|
||||||
pitchClasses =
|
|
||||||
if List.member pitchClass model.whitelistedPitchClasses then
|
|
||||||
List.filter ((/=) pitchClass) model.whitelistedPitchClasses
|
|
||||||
|
|
||||||
else
|
|
||||||
pitchClass :: model.whitelistedPitchClasses
|
|
||||||
in
|
|
||||||
( { model
|
|
||||||
| whitelistedPitchClasses = pitchClasses
|
|
||||||
, whitelistedChords =
|
|
||||||
Theory.allChords
|
|
||||||
{ start = model.firstNote
|
|
||||||
, end = model.lastNote
|
|
||||||
, inversions = model.whitelistedInversions
|
|
||||||
, chordTypes = model.whitelistedChordTypes
|
|
||||||
, pitchClasses = pitchClasses
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
ToggleKey key ->
|
|
||||||
let
|
|
||||||
keys =
|
|
||||||
if List.member key model.whitelistedKeys then
|
|
||||||
List.filter ((/=) key) model.whitelistedKeys
|
|
||||||
|
|
||||||
else
|
|
||||||
key :: model.whitelistedKeys
|
|
||||||
in
|
|
||||||
( { model
|
|
||||||
| whitelistedKeys = keys
|
|
||||||
, whitelistedChords =
|
|
||||||
keys |> List.concatMap Theory.chordsForKey
|
|
||||||
}
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
SetTempo tempo ->
|
|
||||||
( { model
|
|
||||||
| tempo =
|
|
||||||
case String.toInt tempo of
|
|
||||||
Just x ->
|
|
||||||
x
|
|
||||||
|
|
||||||
Nothing ->
|
|
||||||
model.tempo
|
|
||||||
}
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
playPause : Model -> Html Msg
|
|
||||||
playPause { isPaused } =
|
|
||||||
if isPaused then
|
|
||||||
button [ onClick Play ] [ text "Play" ]
|
|
||||||
|
|
||||||
else
|
|
||||||
button [ onClick Pause ] [ text "Pause" ]
|
|
||||||
|
|
||||||
|
|
||||||
chordTypeCheckboxes : List Theory.ChordType -> Html Msg
|
|
||||||
chordTypeCheckboxes chordTypes =
|
|
||||||
ul []
|
|
||||||
(Theory.allChordTypes
|
|
||||||
|> List.map
|
|
||||||
(\chordType ->
|
|
||||||
li []
|
|
||||||
[ label [] [ text (Theory.chordTypeName chordType) ]
|
|
||||||
, input
|
|
||||||
[ type_ "checkbox"
|
|
||||||
, onClick (ToggleChordType chordType)
|
|
||||||
, checked (List.member chordType chordTypes)
|
|
||||||
]
|
|
||||||
[]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
inversionCheckboxes : List Theory.ChordInversion -> Html Msg
|
|
||||||
inversionCheckboxes inversions =
|
|
||||||
ul []
|
|
||||||
(Theory.allInversions
|
|
||||||
|> List.map
|
|
||||||
(\inversion ->
|
|
||||||
li []
|
|
||||||
[ label [] [ text (Theory.inversionName inversion) ]
|
|
||||||
, input
|
|
||||||
[ type_ "checkbox"
|
|
||||||
, onClick (ToggleInversion inversion)
|
|
||||||
, checked (List.member inversion inversions)
|
|
||||||
]
|
|
||||||
[]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
selectKey :
|
|
||||||
Model
|
|
||||||
->
|
|
||||||
{ relativeMajor : Theory.Key
|
|
||||||
, relativeMinor : Theory.Key
|
|
||||||
}
|
|
||||||
-> Html Msg
|
|
||||||
selectKey model { relativeMajor, relativeMinor } =
|
|
||||||
let
|
|
||||||
active key =
|
|
||||||
List.member key model.whitelistedKeys
|
|
||||||
|
|
||||||
buttonLabel major minor =
|
|
||||||
Theory.viewKey major ++ ", " ++ Theory.viewKey minor
|
|
||||||
in
|
|
||||||
div [ class "flex pt-0" ]
|
|
||||||
[ UI.textToggleButton
|
|
||||||
{ label = buttonLabel relativeMajor relativeMinor
|
|
||||||
, handleClick = ToggleKey relativeMinor
|
|
||||||
, classes = [ "flex-1" ]
|
|
||||||
, toggled = active relativeMinor || active relativeMajor
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
keyCheckboxes : Model -> Html Msg
|
|
||||||
keyCheckboxes model =
|
|
||||||
let
|
|
||||||
majorKey pitchClass =
|
|
||||||
{ pitchClass = pitchClass, mode = Theory.MajorMode }
|
|
||||||
|
|
||||||
minorKey pitchClass =
|
|
||||||
{ pitchClass = pitchClass, mode = Theory.MinorMode }
|
|
||||||
|
|
||||||
circleOfFifths =
|
|
||||||
[ ( Theory.C, Theory.A )
|
|
||||||
, ( Theory.G, Theory.E )
|
|
||||||
, ( Theory.D, Theory.B )
|
|
||||||
, ( Theory.A, Theory.F_sharp )
|
|
||||||
, ( Theory.E, Theory.C_sharp )
|
|
||||||
, ( Theory.B, Theory.G_sharp )
|
|
||||||
, ( Theory.F_sharp, Theory.D_sharp )
|
|
||||||
, ( Theory.C_sharp, Theory.A_sharp )
|
|
||||||
, ( Theory.G_sharp, Theory.F )
|
|
||||||
, ( Theory.D_sharp, Theory.C )
|
|
||||||
, ( Theory.A_sharp, Theory.G )
|
|
||||||
, ( Theory.F, Theory.D )
|
|
||||||
]
|
|
||||||
in
|
|
||||||
div []
|
|
||||||
[ h2 [ class "text-gray-500 text-center pt-10 text-5xl" ] [ text "Select keys" ]
|
|
||||||
, ul []
|
|
||||||
(circleOfFifths
|
|
||||||
|> List.map
|
|
||||||
(\( major, minor ) ->
|
|
||||||
selectKey model
|
|
||||||
{ relativeMajor = majorKey major
|
|
||||||
, relativeMinor = minorKey minor
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
practiceModeButtons : Model -> Html Msg
|
|
||||||
practiceModeButtons model =
|
|
||||||
div [ class "text-center" ]
|
|
||||||
[ h2 [ class "py-10 text-5xl" ] [ text "Practice Mode" ]
|
|
||||||
, div [ class "flex pb-6" ]
|
|
||||||
[ UI.simpleButton
|
|
||||||
{ label = "Key"
|
|
||||||
, classes = [ "flex-1", "rounded-r-none" ]
|
|
||||||
, handleClick = SetPracticeMode KeyMode
|
|
||||||
, color =
|
|
||||||
if model.practiceMode == KeyMode then
|
|
||||||
UI.Primary
|
|
||||||
|
|
||||||
else
|
|
||||||
UI.Secondary
|
|
||||||
}
|
|
||||||
, UI.simpleButton
|
|
||||||
{ label = "Fine Tune"
|
|
||||||
, handleClick = SetPracticeMode FineTuneMode
|
|
||||||
, classes = [ "flex-1", "rounded-l-none" ]
|
|
||||||
, color =
|
|
||||||
if model.practiceMode == FineTuneMode then
|
|
||||||
UI.Primary
|
|
||||||
|
|
||||||
else
|
|
||||||
UI.Secondary
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
openPreferences : Html Msg
|
|
||||||
openPreferences =
|
|
||||||
button
|
|
||||||
[ class "w-48 h-48 absolute left-0 top-0 z-40"
|
|
||||||
, onClick (SetView Preferences)
|
|
||||||
]
|
|
||||||
[ Icon.cog ]
|
|
||||||
|
|
||||||
|
|
||||||
closePreferences : Html Msg
|
|
||||||
closePreferences =
|
|
||||||
button
|
|
||||||
[ class "w-48 h-48 absolute right-0 top-0 z-10"
|
|
||||||
, onClick (SetView Practice)
|
|
||||||
]
|
|
||||||
[ Icon.close ]
|
|
||||||
|
|
||||||
|
|
||||||
preferences : Model -> Html Msg
|
|
||||||
preferences model =
|
|
||||||
div [ class "pt-10 pb-20 px-10" ]
|
|
||||||
[ closePreferences
|
|
||||||
, Tempo.render
|
|
||||||
{ tempo = model.tempo
|
|
||||||
, handleInput = SetTempo
|
|
||||||
}
|
|
||||||
, case model.practiceMode of
|
|
||||||
KeyMode ->
|
|
||||||
keyCheckboxes model
|
|
||||||
|
|
||||||
FineTuneMode ->
|
|
||||||
div []
|
|
||||||
[ inversionCheckboxes model.whitelistedInversions
|
|
||||||
, chordTypeCheckboxes model.whitelistedChordTypes
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
practice : Model -> Html Msg
|
|
||||||
practice model =
|
|
||||||
let
|
|
||||||
( handleClick, buttonText ) =
|
|
||||||
if model.isPaused then
|
|
||||||
( Play, "Press to practice" )
|
|
||||||
|
|
||||||
else
|
|
||||||
( Pause, "" )
|
|
||||||
in
|
|
||||||
div []
|
|
||||||
[ openPreferences
|
|
||||||
, UI.overlayButton
|
|
||||||
{ label = buttonText
|
|
||||||
, handleClick = handleClick
|
|
||||||
, isVisible = model.isPaused
|
|
||||||
}
|
|
||||||
, Piano.render
|
|
||||||
{ highlight = model.selectedChord |> Maybe.andThen Theory.notesForChord |> Maybe.withDefault []
|
|
||||||
, start = model.firstNote
|
|
||||||
, end = model.lastNote
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
view : Model -> Html Msg
|
|
||||||
view model =
|
view model =
|
||||||
case model.view of
|
case model.view of
|
||||||
Preferences ->
|
State.Preferences ->
|
||||||
preferences model
|
Preferences.render model
|
||||||
|
|
||||||
Practice ->
|
State.Practice ->
|
||||||
practice model
|
Practice.render model
|
||||||
|
|
||||||
|
State.Overview ->
|
||||||
|
Overview.render model
|
||||||
|
|
||||||
|
|
||||||
{-| For now, I'm just dumping things onto the page to sketch ideas.
|
|
||||||
-}
|
|
||||||
main =
|
main =
|
||||||
Browser.element
|
Browser.element
|
||||||
{ init = \() -> ( init, Cmd.none )
|
{ init = \() -> ( State.init, Cmd.none )
|
||||||
, subscriptions = subscriptions
|
, subscriptions = subscriptions
|
||||||
, update = update
|
, update = State.update
|
||||||
, view = view
|
, view = view
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,3 +45,15 @@ find pred xs =
|
||||||
|
|
||||||
x :: _ ->
|
x :: _ ->
|
||||||
Just x
|
Just x
|
||||||
|
|
||||||
|
|
||||||
|
{-| Return the number of milliseconds that elapse during an interval in a
|
||||||
|
`target` bpm.
|
||||||
|
-}
|
||||||
|
bpmToMilliseconds : Int -> Int
|
||||||
|
bpmToMilliseconds target =
|
||||||
|
let
|
||||||
|
msPerMinute =
|
||||||
|
1000 * 60
|
||||||
|
in
|
||||||
|
round (toFloat msPerMinute / toFloat target)
|
||||||
|
|
11
website/sandbox/learnpianochords/src/Overview.elm
Normal file
11
website/sandbox/learnpianochords/src/Overview.elm
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module Overview exposing (render)
|
||||||
|
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes exposing (..)
|
||||||
|
import Html.Events exposing (..)
|
||||||
|
import State
|
||||||
|
|
||||||
|
|
||||||
|
render : State.Model -> Html State.Msg
|
||||||
|
render model =
|
||||||
|
div [] [ text "Hello, Overview" ]
|
44
website/sandbox/learnpianochords/src/Practice.elm
Normal file
44
website/sandbox/learnpianochords/src/Practice.elm
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
module Practice exposing (render)
|
||||||
|
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes exposing (..)
|
||||||
|
import Html.Events exposing (..)
|
||||||
|
import Icon
|
||||||
|
import Piano
|
||||||
|
import State
|
||||||
|
import Theory
|
||||||
|
import UI
|
||||||
|
|
||||||
|
|
||||||
|
openPreferences : Html State.Msg
|
||||||
|
openPreferences =
|
||||||
|
button
|
||||||
|
[ class "w-48 h-48 absolute left-0 top-0 z-40"
|
||||||
|
, onClick (State.SetView State.Preferences)
|
||||||
|
]
|
||||||
|
[ Icon.cog ]
|
||||||
|
|
||||||
|
|
||||||
|
render : State.Model -> Html State.Msg
|
||||||
|
render model =
|
||||||
|
let
|
||||||
|
( handleClick, buttonText ) =
|
||||||
|
if model.isPaused then
|
||||||
|
( State.Play, "Press to practice" )
|
||||||
|
|
||||||
|
else
|
||||||
|
( State.Pause, "" )
|
||||||
|
in
|
||||||
|
div []
|
||||||
|
[ openPreferences
|
||||||
|
, UI.overlayButton
|
||||||
|
{ label = buttonText
|
||||||
|
, handleClick = handleClick
|
||||||
|
, isVisible = model.isPaused
|
||||||
|
}
|
||||||
|
, Piano.render
|
||||||
|
{ highlight = model.selectedChord |> Maybe.andThen Theory.notesForChord |> Maybe.withDefault []
|
||||||
|
, start = model.firstNote
|
||||||
|
, end = model.lastNote
|
||||||
|
}
|
||||||
|
]
|
141
website/sandbox/learnpianochords/src/Preferences.elm
Normal file
141
website/sandbox/learnpianochords/src/Preferences.elm
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
module Preferences exposing (render)
|
||||||
|
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes exposing (..)
|
||||||
|
import Html.Events exposing (..)
|
||||||
|
import Icon
|
||||||
|
import State
|
||||||
|
import Tempo
|
||||||
|
import Theory
|
||||||
|
import UI
|
||||||
|
|
||||||
|
|
||||||
|
selectKey :
|
||||||
|
State.Model
|
||||||
|
->
|
||||||
|
{ relativeMajor : Theory.Key
|
||||||
|
, relativeMinor : Theory.Key
|
||||||
|
}
|
||||||
|
-> Html State.Msg
|
||||||
|
selectKey model { relativeMajor, relativeMinor } =
|
||||||
|
let
|
||||||
|
active key =
|
||||||
|
List.member key model.whitelistedKeys
|
||||||
|
|
||||||
|
buttonLabel major minor =
|
||||||
|
Theory.viewKey major ++ ", " ++ Theory.viewKey minor
|
||||||
|
in
|
||||||
|
div [ class "flex pt-0" ]
|
||||||
|
[ UI.textToggleButton
|
||||||
|
{ label = buttonLabel relativeMajor relativeMinor
|
||||||
|
, handleClick = State.ToggleKey relativeMinor
|
||||||
|
, classes = [ "flex-1" ]
|
||||||
|
, toggled = active relativeMinor || active relativeMajor
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
chordTypeCheckboxes : List Theory.ChordType -> Html State.Msg
|
||||||
|
chordTypeCheckboxes chordTypes =
|
||||||
|
ul []
|
||||||
|
(Theory.allChordTypes
|
||||||
|
|> List.map
|
||||||
|
(\chordType ->
|
||||||
|
li []
|
||||||
|
[ label [] [ text (Theory.chordTypeName chordType) ]
|
||||||
|
, input
|
||||||
|
[ type_ "checkbox"
|
||||||
|
, onClick (State.ToggleChordType chordType)
|
||||||
|
, checked (List.member chordType chordTypes)
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
inversionCheckboxes : List Theory.ChordInversion -> Html State.Msg
|
||||||
|
inversionCheckboxes inversions =
|
||||||
|
ul []
|
||||||
|
(Theory.allInversions
|
||||||
|
|> List.map
|
||||||
|
(\inversion ->
|
||||||
|
li []
|
||||||
|
[ label [] [ text (Theory.inversionName inversion) ]
|
||||||
|
, input
|
||||||
|
[ type_ "checkbox"
|
||||||
|
, onClick (State.ToggleInversion inversion)
|
||||||
|
, checked (List.member inversion inversions)
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
keyCheckboxes : State.Model -> Html State.Msg
|
||||||
|
keyCheckboxes model =
|
||||||
|
let
|
||||||
|
majorKey pitchClass =
|
||||||
|
{ pitchClass = pitchClass, mode = Theory.MajorMode }
|
||||||
|
|
||||||
|
minorKey pitchClass =
|
||||||
|
{ pitchClass = pitchClass, mode = Theory.MinorMode }
|
||||||
|
|
||||||
|
circleOfFifths =
|
||||||
|
[ ( Theory.C, Theory.A )
|
||||||
|
, ( Theory.G, Theory.E )
|
||||||
|
, ( Theory.D, Theory.B )
|
||||||
|
, ( Theory.A, Theory.F_sharp )
|
||||||
|
, ( Theory.E, Theory.C_sharp )
|
||||||
|
, ( Theory.B, Theory.G_sharp )
|
||||||
|
, ( Theory.F_sharp, Theory.D_sharp )
|
||||||
|
, ( Theory.C_sharp, Theory.A_sharp )
|
||||||
|
, ( Theory.G_sharp, Theory.F )
|
||||||
|
, ( Theory.D_sharp, Theory.C )
|
||||||
|
, ( Theory.A_sharp, Theory.G )
|
||||||
|
, ( Theory.F, Theory.D )
|
||||||
|
]
|
||||||
|
in
|
||||||
|
div []
|
||||||
|
[ h2 [ class "text-gray-500 text-center pt-10 text-5xl" ] [ text "Select keys" ]
|
||||||
|
, ul []
|
||||||
|
(circleOfFifths
|
||||||
|
|> List.map
|
||||||
|
(\( major, minor ) ->
|
||||||
|
selectKey model
|
||||||
|
{ relativeMajor = majorKey major
|
||||||
|
, relativeMinor = minorKey minor
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
closePreferences : Html State.Msg
|
||||||
|
closePreferences =
|
||||||
|
button
|
||||||
|
[ class "w-48 h-48 absolute right-0 top-0 z-10"
|
||||||
|
, onClick (State.SetView State.Practice)
|
||||||
|
]
|
||||||
|
[ Icon.close ]
|
||||||
|
|
||||||
|
|
||||||
|
render : State.Model -> Html State.Msg
|
||||||
|
render model =
|
||||||
|
div [ class "pt-10 pb-20 px-10" ]
|
||||||
|
[ closePreferences
|
||||||
|
, Tempo.render
|
||||||
|
{ tempo = model.tempo
|
||||||
|
, handleInput = State.SetTempo
|
||||||
|
}
|
||||||
|
, case model.practiceMode of
|
||||||
|
State.KeyMode ->
|
||||||
|
keyCheckboxes model
|
||||||
|
|
||||||
|
State.FineTuneMode ->
|
||||||
|
div []
|
||||||
|
[ inversionCheckboxes model.whitelistedInversions
|
||||||
|
, chordTypeCheckboxes model.whitelistedChordTypes
|
||||||
|
]
|
||||||
|
]
|
271
website/sandbox/learnpianochords/src/State.elm
Normal file
271
website/sandbox/learnpianochords/src/State.elm
Normal file
|
@ -0,0 +1,271 @@
|
||||||
|
module State exposing (..)
|
||||||
|
|
||||||
|
import Random
|
||||||
|
import Random.List
|
||||||
|
import Theory
|
||||||
|
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= NextChord
|
||||||
|
| NewChord Theory.Chord
|
||||||
|
| Play
|
||||||
|
| Pause
|
||||||
|
| SetTempo String
|
||||||
|
| ToggleInversion Theory.ChordInversion
|
||||||
|
| ToggleChordType Theory.ChordType
|
||||||
|
| TogglePitchClass Theory.PitchClass
|
||||||
|
| ToggleKey Theory.Key
|
||||||
|
| DoNothing
|
||||||
|
| SetPracticeMode PracticeMode
|
||||||
|
| SelectAllKeys
|
||||||
|
| DeselectAllKeys
|
||||||
|
| SetView View
|
||||||
|
|
||||||
|
|
||||||
|
type View
|
||||||
|
= Preferences
|
||||||
|
| Practice
|
||||||
|
| Overview
|
||||||
|
|
||||||
|
|
||||||
|
{-| Control the type of practice you'd like.
|
||||||
|
-}
|
||||||
|
type PracticeMode
|
||||||
|
= KeyMode
|
||||||
|
| FineTuneMode
|
||||||
|
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
{ whitelistedChords : List Theory.Chord
|
||||||
|
, whitelistedChordTypes : List Theory.ChordType
|
||||||
|
, whitelistedInversions : List Theory.ChordInversion
|
||||||
|
, whitelistedPitchClasses : List Theory.PitchClass
|
||||||
|
, whitelistedKeys : List Theory.Key
|
||||||
|
, selectedChord : Maybe Theory.Chord
|
||||||
|
, isPaused : Bool
|
||||||
|
, tempo : Int
|
||||||
|
, firstNote : Theory.Note
|
||||||
|
, lastNote : Theory.Note
|
||||||
|
, practiceMode : PracticeMode
|
||||||
|
, view : View
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| The initial state for the application.
|
||||||
|
-}
|
||||||
|
init : Model
|
||||||
|
init =
|
||||||
|
let
|
||||||
|
( firstNote, lastNote ) =
|
||||||
|
( Theory.C3, Theory.C6 )
|
||||||
|
|
||||||
|
inversions =
|
||||||
|
Theory.allInversions
|
||||||
|
|
||||||
|
chordTypes =
|
||||||
|
Theory.allChordTypes
|
||||||
|
|
||||||
|
pitchClasses =
|
||||||
|
Theory.allPitchClasses
|
||||||
|
|
||||||
|
keys =
|
||||||
|
[ { pitchClass = Theory.C, mode = Theory.MajorMode } ]
|
||||||
|
|
||||||
|
practiceMode =
|
||||||
|
KeyMode
|
||||||
|
in
|
||||||
|
{ practiceMode = practiceMode
|
||||||
|
, whitelistedChords =
|
||||||
|
case practiceMode of
|
||||||
|
KeyMode ->
|
||||||
|
keys |> List.concatMap Theory.chordsForKey
|
||||||
|
|
||||||
|
FineTuneMode ->
|
||||||
|
Theory.allChords
|
||||||
|
{ start = firstNote
|
||||||
|
, end = lastNote
|
||||||
|
, inversions = inversions
|
||||||
|
, chordTypes = chordTypes
|
||||||
|
, pitchClasses = pitchClasses
|
||||||
|
}
|
||||||
|
, whitelistedChordTypes = chordTypes
|
||||||
|
, whitelistedInversions = inversions
|
||||||
|
, whitelistedPitchClasses = pitchClasses
|
||||||
|
, whitelistedKeys = keys
|
||||||
|
, selectedChord = Nothing
|
||||||
|
, isPaused = True
|
||||||
|
, tempo = 20
|
||||||
|
, firstNote = firstNote
|
||||||
|
, lastNote = lastNote
|
||||||
|
, view = Preferences
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Now that we have state, we need a function to change the state.
|
||||||
|
-}
|
||||||
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
|
update msg model =
|
||||||
|
case msg of
|
||||||
|
DoNothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
SetPracticeMode practiceMode ->
|
||||||
|
( { model
|
||||||
|
| practiceMode = practiceMode
|
||||||
|
, isPaused = True
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
SetView x ->
|
||||||
|
( { model
|
||||||
|
| view = x
|
||||||
|
, isPaused = True
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
SelectAllKeys ->
|
||||||
|
( { model
|
||||||
|
| whitelistedKeys = Theory.allKeys
|
||||||
|
, whitelistedChords =
|
||||||
|
Theory.allKeys |> List.concatMap Theory.chordsForKey
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
DeselectAllKeys ->
|
||||||
|
( { model
|
||||||
|
| whitelistedKeys = []
|
||||||
|
, whitelistedChords = []
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
NewChord chord ->
|
||||||
|
( { model | selectedChord = Just chord }
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
NextChord ->
|
||||||
|
( model
|
||||||
|
, Random.generate
|
||||||
|
(\x ->
|
||||||
|
case x of
|
||||||
|
( Just chord, _ ) ->
|
||||||
|
NewChord chord
|
||||||
|
|
||||||
|
( Nothing, _ ) ->
|
||||||
|
DoNothing
|
||||||
|
)
|
||||||
|
(Random.List.choose model.whitelistedChords)
|
||||||
|
)
|
||||||
|
|
||||||
|
Play ->
|
||||||
|
( { model | isPaused = False }
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
Pause ->
|
||||||
|
( { model | isPaused = True }
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
ToggleChordType chordType ->
|
||||||
|
let
|
||||||
|
chordTypes =
|
||||||
|
if List.member chordType model.whitelistedChordTypes then
|
||||||
|
List.filter ((/=) chordType) model.whitelistedChordTypes
|
||||||
|
|
||||||
|
else
|
||||||
|
chordType :: model.whitelistedChordTypes
|
||||||
|
in
|
||||||
|
( { model
|
||||||
|
| whitelistedChordTypes = chordTypes
|
||||||
|
, whitelistedChords =
|
||||||
|
Theory.allChords
|
||||||
|
{ start = model.firstNote
|
||||||
|
, end = model.lastNote
|
||||||
|
, inversions = model.whitelistedInversions
|
||||||
|
, chordTypes = chordTypes
|
||||||
|
, pitchClasses = model.whitelistedPitchClasses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
ToggleInversion inversion ->
|
||||||
|
let
|
||||||
|
inversions =
|
||||||
|
if List.member inversion model.whitelistedInversions then
|
||||||
|
List.filter ((/=) inversion) model.whitelistedInversions
|
||||||
|
|
||||||
|
else
|
||||||
|
inversion :: model.whitelistedInversions
|
||||||
|
in
|
||||||
|
( { model
|
||||||
|
| whitelistedInversions = inversions
|
||||||
|
, whitelistedChords =
|
||||||
|
Theory.allChords
|
||||||
|
{ start = model.firstNote
|
||||||
|
, end = model.lastNote
|
||||||
|
, inversions = inversions
|
||||||
|
, chordTypes = model.whitelistedChordTypes
|
||||||
|
, pitchClasses = model.whitelistedPitchClasses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
TogglePitchClass pitchClass ->
|
||||||
|
let
|
||||||
|
pitchClasses =
|
||||||
|
if List.member pitchClass model.whitelistedPitchClasses then
|
||||||
|
List.filter ((/=) pitchClass) model.whitelistedPitchClasses
|
||||||
|
|
||||||
|
else
|
||||||
|
pitchClass :: model.whitelistedPitchClasses
|
||||||
|
in
|
||||||
|
( { model
|
||||||
|
| whitelistedPitchClasses = pitchClasses
|
||||||
|
, whitelistedChords =
|
||||||
|
Theory.allChords
|
||||||
|
{ start = model.firstNote
|
||||||
|
, end = model.lastNote
|
||||||
|
, inversions = model.whitelistedInversions
|
||||||
|
, chordTypes = model.whitelistedChordTypes
|
||||||
|
, pitchClasses = pitchClasses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
ToggleKey key ->
|
||||||
|
let
|
||||||
|
keys =
|
||||||
|
if List.member key model.whitelistedKeys then
|
||||||
|
List.filter ((/=) key) model.whitelistedKeys
|
||||||
|
|
||||||
|
else
|
||||||
|
key :: model.whitelistedKeys
|
||||||
|
in
|
||||||
|
( { model
|
||||||
|
| whitelistedKeys = keys
|
||||||
|
, whitelistedChords =
|
||||||
|
keys |> List.concatMap Theory.chordsForKey
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
SetTempo tempo ->
|
||||||
|
( { model
|
||||||
|
| tempo =
|
||||||
|
case String.toInt tempo of
|
||||||
|
Just x ->
|
||||||
|
x
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
model.tempo
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
Loading…
Reference in a new issue