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 Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (..)
|
||||
import Icon
|
||||
import Piano
|
||||
import Random
|
||||
import Random.List
|
||||
import Tempo
|
||||
import Theory
|
||||
import Misc
|
||||
import Overview
|
||||
import Practice
|
||||
import Preferences
|
||||
import State
|
||||
import Time exposing (..)
|
||||
import UI
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
subscriptions : State.Model -> Sub State.Msg
|
||||
subscriptions model =
|
||||
if model.isPaused then
|
||||
Sub.none
|
||||
|
||||
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.
|
||||
-}
|
||||
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 : State.Model -> Html State.Msg
|
||||
view model =
|
||||
case model.view of
|
||||
Preferences ->
|
||||
preferences model
|
||||
State.Preferences ->
|
||||
Preferences.render model
|
||||
|
||||
Practice ->
|
||||
practice model
|
||||
State.Practice ->
|
||||
Practice.render model
|
||||
|
||||
State.Overview ->
|
||||
Overview.render model
|
||||
|
||||
|
||||
{-| For now, I'm just dumping things onto the page to sketch ideas.
|
||||
-}
|
||||
main =
|
||||
Browser.element
|
||||
{ init = \() -> ( init, Cmd.none )
|
||||
{ init = \() -> ( State.init, Cmd.none )
|
||||
, subscriptions = subscriptions
|
||||
, update = update
|
||||
, update = State.update
|
||||
, view = view
|
||||
}
|
||||
|
|
|
@ -45,3 +45,15 @@ find pred xs =
|
|||
|
||||
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