about summary refs log tree commit diff
path: root/website/sandbox/learnpianochords
diff options
context:
space:
mode:
authorWilliam Carroll <wpcarro@gmail.com>2020-04-18T13·58+0100
committerWilliam Carroll <wpcarro@gmail.com>2020-04-18T13·58+0100
commit441fe3e32eb9c041515178f74564ede9fd24db72 (patch)
tree8264b2cdf597cf0063214818467f6f849661220a /website/sandbox/learnpianochords
parentddbd7e2ef5eb52bf125480bad80ee753275d9827 (diff)
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
Diffstat (limited to 'website/sandbox/learnpianochords')
-rw-r--r--website/sandbox/learnpianochords/src/ChordInspector.elm15
-rw-r--r--website/sandbox/learnpianochords/src/Main.elm542
-rw-r--r--website/sandbox/learnpianochords/src/Misc.elm12
-rw-r--r--website/sandbox/learnpianochords/src/Overview.elm11
-rw-r--r--website/sandbox/learnpianochords/src/Practice.elm44
-rw-r--r--website/sandbox/learnpianochords/src/Preferences.elm141
-rw-r--r--website/sandbox/learnpianochords/src/State.elm271
7 files changed, 498 insertions, 538 deletions
diff --git a/website/sandbox/learnpianochords/src/ChordInspector.elm b/website/sandbox/learnpianochords/src/ChordInspector.elm
deleted file mode 100644
index f43b534eb013..000000000000
--- a/website/sandbox/learnpianochords/src/ChordInspector.elm
+++ /dev/null
@@ -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
diff --git a/website/sandbox/learnpianochords/src/Main.elm b/website/sandbox/learnpianochords/src/Main.elm
index 1c40d0d7961a..d583e3fcf9a3 100644
--- a/website/sandbox/learnpianochords/src/Main.elm
+++ b/website/sandbox/learnpianochords/src/Main.elm
@@ -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)
-
-
-{-| 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
-            }
-        ]
-
+        Time.every (model.tempo |> Misc.bpmToMilliseconds |> toFloat) (\_ -> State.NextChord)
 
-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
+
+        State.Practice ->
+            Practice.render model
 
-        Practice ->
-            practice 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
         }
diff --git a/website/sandbox/learnpianochords/src/Misc.elm b/website/sandbox/learnpianochords/src/Misc.elm
index 52f957ad528f..288d7a825f4b 100644
--- a/website/sandbox/learnpianochords/src/Misc.elm
+++ b/website/sandbox/learnpianochords/src/Misc.elm
@@ -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)
diff --git a/website/sandbox/learnpianochords/src/Overview.elm b/website/sandbox/learnpianochords/src/Overview.elm
new file mode 100644
index 000000000000..432477aa3bb9
--- /dev/null
+++ b/website/sandbox/learnpianochords/src/Overview.elm
@@ -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" ]
diff --git a/website/sandbox/learnpianochords/src/Practice.elm b/website/sandbox/learnpianochords/src/Practice.elm
new file mode 100644
index 000000000000..229f019aa030
--- /dev/null
+++ b/website/sandbox/learnpianochords/src/Practice.elm
@@ -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
+            }
+        ]
diff --git a/website/sandbox/learnpianochords/src/Preferences.elm b/website/sandbox/learnpianochords/src/Preferences.elm
new file mode 100644
index 000000000000..c0288e1265e7
--- /dev/null
+++ b/website/sandbox/learnpianochords/src/Preferences.elm
@@ -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
+                    ]
+        ]
diff --git a/website/sandbox/learnpianochords/src/State.elm b/website/sandbox/learnpianochords/src/State.elm
new file mode 100644
index 000000000000..2588d8566ff1
--- /dev/null
+++ b/website/sandbox/learnpianochords/src/State.elm
@@ -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
+            )