2020-04-11 00:03:01 +02:00
|
|
|
module Main exposing (main)
|
|
|
|
|
|
|
|
import Browser
|
|
|
|
import Html exposing (..)
|
|
|
|
import Html.Attributes exposing (..)
|
|
|
|
import Html.Events exposing (..)
|
2020-04-17 13:38:08 +02:00
|
|
|
import Icon
|
2020-04-12 17:43:34 +02:00
|
|
|
import Piano
|
2020-04-11 11:45:42 +02:00
|
|
|
import Random
|
|
|
|
import Random.List
|
2020-04-12 17:43:34 +02:00
|
|
|
import Tempo
|
|
|
|
import Theory
|
2020-04-11 17:09:11 +02:00
|
|
|
import Time exposing (..)
|
2020-04-13 23:39:15 +02:00
|
|
|
import UI
|
2020-04-11 00:03:01 +02:00
|
|
|
|
2020-04-11 11:45:42 +02:00
|
|
|
|
Prefer type alias to type
Elm reminds me of Haskell. In fact, I'm using `haskell-mode` (for now) in Emacs
to write my Elm code, and it works reliably. I'm not writing a Haskell app, but
if I were, I would define my application Model with the following Haskell code:
```haskell
data Model = Model { whitelistedChords :: [Theory.Chord]
, selectedChord :: Theory.Chord
, isPaused :: Bool
, tempo :: Int
}
```
When I first modelled my application state, I did something similar. After
reading more Elm examples of SPAs, I see that people prefer using type aliases
to define records. As far as I know, you cannot do this in Haskell; I believe
all types are "tagged" (something about "nominal typing" comes to mind). Anyhow,
Elm isn't Haskell; Haskell has cool features like type classes; Elm has cool
features like human-readable error messages and exhaustiveness checking for
cases. I love Haskell, and I love Elm, and you didn't ask.
Anyhow, this commit refactors my records as type aliases instead of types. I
think the resulting code is more readable and ergonomic.
2020-04-11 17:50:02 +02:00
|
|
|
type alias Model =
|
2020-04-12 17:43:34 +02:00
|
|
|
{ whitelistedChords : List Theory.Chord
|
2020-04-12 20:32:54 +02:00
|
|
|
, whitelistedChordTypes : List Theory.ChordType
|
|
|
|
, whitelistedInversions : List Theory.ChordInversion
|
2020-04-13 11:02:03 +02:00
|
|
|
, whitelistedPitchClasses : List Theory.PitchClass
|
2020-04-13 16:07:03 +02:00
|
|
|
, whitelistedKeys : List Theory.Key
|
2020-04-13 10:42:26 +02:00
|
|
|
, selectedChord : Maybe Theory.Chord
|
2020-04-12 17:43:34 +02:00
|
|
|
, isPaused : Bool
|
|
|
|
, tempo : Int
|
|
|
|
, firstNote : Theory.Note
|
|
|
|
, lastNote : Theory.Note
|
2020-04-13 16:07:03 +02:00
|
|
|
, practiceMode : PracticeMode
|
2020-04-13 23:39:15 +02:00
|
|
|
, view : View
|
2020-04-12 17:43:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-04-13 23:39:15 +02:00
|
|
|
type View
|
|
|
|
= Preferences
|
|
|
|
| Practice
|
|
|
|
|
|
|
|
|
2020-04-13 16:07:03 +02:00
|
|
|
{-| Control the type of practice you'd like.
|
|
|
|
-}
|
|
|
|
type PracticeMode
|
|
|
|
= KeyMode
|
|
|
|
| FineTuneMode
|
|
|
|
|
|
|
|
|
2020-04-12 17:43:34 +02:00
|
|
|
type Msg
|
|
|
|
= NextChord
|
|
|
|
| NewChord Theory.Chord
|
|
|
|
| Play
|
|
|
|
| Pause
|
|
|
|
| IncreaseTempo
|
|
|
|
| DecreaseTempo
|
|
|
|
| SetTempo String
|
2020-04-12 20:20:00 +02:00
|
|
|
| ToggleInversion Theory.ChordInversion
|
2020-04-12 20:32:54 +02:00
|
|
|
| ToggleChordType Theory.ChordType
|
2020-04-13 11:02:03 +02:00
|
|
|
| TogglePitchClass Theory.PitchClass
|
2020-04-13 16:07:03 +02:00
|
|
|
| ToggleKey Theory.Key
|
2020-04-13 10:42:26 +02:00
|
|
|
| DoNothing
|
2020-04-13 16:07:03 +02:00
|
|
|
| SetPracticeMode PracticeMode
|
|
|
|
| SelectAllKeys
|
|
|
|
| DeselectAllKeys
|
2020-04-17 13:38:08 +02:00
|
|
|
| SetView View
|
2020-04-12 17:43:34 +02:00
|
|
|
|
2020-04-11 17:09:11 +02:00
|
|
|
|
2020-04-13 10:42:26 +02:00
|
|
|
{-| The amount by which we increase or decrease tempo.
|
|
|
|
-}
|
2020-04-11 17:09:11 +02:00
|
|
|
tempoStep : Int
|
2020-04-12 17:43:34 +02:00
|
|
|
tempoStep =
|
|
|
|
5
|
|
|
|
|
2020-04-11 18:46:46 +02:00
|
|
|
|
|
|
|
{-| Return the number of milliseconds that elapse during an interval in a
|
|
|
|
`target` bpm.
|
|
|
|
-}
|
|
|
|
bpmToMilliseconds : Int -> Int
|
|
|
|
bpmToMilliseconds target =
|
2020-04-12 17:43:34 +02:00
|
|
|
let
|
|
|
|
msPerMinute =
|
|
|
|
1000 * 60
|
|
|
|
in
|
|
|
|
round (toFloat msPerMinute / toFloat target)
|
|
|
|
|
2020-04-11 11:45:42 +02:00
|
|
|
|
2020-04-12 17:43:34 +02:00
|
|
|
{-| The initial state for the application.
|
|
|
|
-}
|
2020-04-11 17:09:11 +02:00
|
|
|
init : Model
|
|
|
|
init =
|
2020-04-12 17:43:34 +02:00
|
|
|
let
|
|
|
|
( firstNote, lastNote ) =
|
2020-04-17 13:38:08 +02:00
|
|
|
( Theory.C3, Theory.C6 )
|
2020-04-13 00:35:16 +02:00
|
|
|
|
|
|
|
inversions =
|
|
|
|
Theory.allInversions
|
|
|
|
|
|
|
|
chordTypes =
|
|
|
|
Theory.allChordTypes
|
|
|
|
|
2020-04-13 11:02:03 +02:00
|
|
|
pitchClasses =
|
|
|
|
Theory.allPitchClasses
|
2020-04-13 16:07:03 +02:00
|
|
|
|
|
|
|
keys =
|
2020-04-18 15:24:41 +02:00
|
|
|
[ { pitchClass = Theory.C, mode = Theory.MajorMode } ]
|
2020-04-13 16:07:03 +02:00
|
|
|
|
|
|
|
practiceMode =
|
|
|
|
KeyMode
|
2020-04-12 17:43:34 +02:00
|
|
|
in
|
2020-04-13 16:07:03 +02:00
|
|
|
{ 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
|
|
|
|
}
|
2020-04-13 00:35:16 +02:00
|
|
|
, whitelistedChordTypes = chordTypes
|
|
|
|
, whitelistedInversions = inversions
|
2020-04-13 11:02:03 +02:00
|
|
|
, whitelistedPitchClasses = pitchClasses
|
2020-04-13 16:07:03 +02:00
|
|
|
, whitelistedKeys = keys
|
2020-04-13 10:42:26 +02:00
|
|
|
, selectedChord = Nothing
|
2020-04-12 17:43:34 +02:00
|
|
|
, isPaused = True
|
2020-04-17 16:08:38 +02:00
|
|
|
, tempo = 20
|
2020-04-12 17:43:34 +02:00
|
|
|
, firstNote = firstNote
|
|
|
|
, lastNote = lastNote
|
2020-04-17 14:35:33 +02:00
|
|
|
, view = Preferences
|
2020-04-12 17:43:34 +02:00
|
|
|
}
|
|
|
|
|
2020-04-11 11:45:42 +02:00
|
|
|
|
2020-04-11 17:09:11 +02:00
|
|
|
subscriptions : Model -> Sub Msg
|
2020-04-12 17:43:34 +02:00
|
|
|
subscriptions { isPaused, tempo } =
|
|
|
|
if 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 )
|
Prefer type alias to type
Elm reminds me of Haskell. In fact, I'm using `haskell-mode` (for now) in Emacs
to write my Elm code, and it works reliably. I'm not writing a Haskell app, but
if I were, I would define my application Model with the following Haskell code:
```haskell
data Model = Model { whitelistedChords :: [Theory.Chord]
, selectedChord :: Theory.Chord
, isPaused :: Bool
, tempo :: Int
}
```
When I first modelled my application state, I did something similar. After
reading more Elm examples of SPAs, I see that people prefer using type aliases
to define records. As far as I know, you cannot do this in Haskell; I believe
all types are "tagged" (something about "nominal typing" comes to mind). Anyhow,
Elm isn't Haskell; Haskell has cool features like type classes; Elm has cool
features like human-readable error messages and exhaustiveness checking for
cases. I love Haskell, and I love Elm, and you didn't ask.
Anyhow, this commit refactors my records as type aliases instead of types. I
think the resulting code is more readable and ergonomic.
2020-04-11 17:50:02 +02:00
|
|
|
update msg model =
|
2020-04-12 17:43:34 +02:00
|
|
|
case msg of
|
2020-04-13 10:42:26 +02:00
|
|
|
DoNothing ->
|
|
|
|
( model, Cmd.none )
|
|
|
|
|
2020-04-13 16:07:03 +02:00
|
|
|
SetPracticeMode practiceMode ->
|
|
|
|
( { model
|
|
|
|
| practiceMode = practiceMode
|
|
|
|
, isPaused = True
|
|
|
|
}
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
2020-04-17 13:38:08 +02:00
|
|
|
SetView x ->
|
|
|
|
( { model
|
|
|
|
| view = x
|
2020-04-17 14:35:33 +02:00
|
|
|
, isPaused = True
|
2020-04-17 13:38:08 +02:00
|
|
|
}
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
2020-04-13 16:07:03 +02:00
|
|
|
SelectAllKeys ->
|
|
|
|
( { model
|
|
|
|
| whitelistedKeys = Theory.allKeys
|
|
|
|
, whitelistedChords =
|
|
|
|
Theory.allKeys |> List.concatMap Theory.chordsForKey
|
|
|
|
}
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
|
|
|
DeselectAllKeys ->
|
|
|
|
( { model
|
|
|
|
| whitelistedKeys = []
|
|
|
|
, whitelistedChords = []
|
|
|
|
}
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
2020-04-12 17:43:34 +02:00
|
|
|
NewChord chord ->
|
2020-04-13 10:42:26 +02:00
|
|
|
( { model | selectedChord = Just chord }
|
2020-04-11 17:09:11 +02:00
|
|
|
, Cmd.none
|
|
|
|
)
|
2020-04-12 17:43:34 +02:00
|
|
|
|
|
|
|
NextChord ->
|
|
|
|
( model
|
|
|
|
, Random.generate
|
|
|
|
(\x ->
|
|
|
|
case x of
|
|
|
|
( Just chord, _ ) ->
|
|
|
|
NewChord chord
|
|
|
|
|
|
|
|
( Nothing, _ ) ->
|
2020-04-13 10:42:26 +02:00
|
|
|
DoNothing
|
2020-04-12 17:43:34 +02:00
|
|
|
)
|
|
|
|
(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
|
|
|
|
)
|
|
|
|
|
2020-04-12 20:32:54 +02:00
|
|
|
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
|
2020-04-13 11:02:03 +02:00
|
|
|
, pitchClasses = model.whitelistedPitchClasses
|
2020-04-12 20:32:54 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
2020-04-12 20:20:00 +02:00
|
|
|
ToggleInversion inversion ->
|
|
|
|
let
|
|
|
|
inversions =
|
|
|
|
if List.member inversion model.whitelistedInversions then
|
|
|
|
List.filter ((/=) inversion) model.whitelistedInversions
|
|
|
|
|
|
|
|
else
|
|
|
|
inversion :: model.whitelistedInversions
|
|
|
|
in
|
|
|
|
( { model
|
|
|
|
| whitelistedInversions = inversions
|
2020-04-12 20:32:54 +02:00
|
|
|
, whitelistedChords =
|
|
|
|
Theory.allChords
|
|
|
|
{ start = model.firstNote
|
|
|
|
, end = model.lastNote
|
|
|
|
, inversions = inversions
|
|
|
|
, chordTypes = model.whitelistedChordTypes
|
2020-04-13 11:02:03 +02:00
|
|
|
, pitchClasses = model.whitelistedPitchClasses
|
2020-04-13 00:35:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
2020-04-13 11:02:03 +02:00
|
|
|
TogglePitchClass pitchClass ->
|
2020-04-13 00:35:16 +02:00
|
|
|
let
|
2020-04-13 11:02:03 +02:00
|
|
|
pitchClasses =
|
|
|
|
if List.member pitchClass model.whitelistedPitchClasses then
|
|
|
|
List.filter ((/=) pitchClass) model.whitelistedPitchClasses
|
2020-04-13 00:35:16 +02:00
|
|
|
|
|
|
|
else
|
2020-04-13 11:02:03 +02:00
|
|
|
pitchClass :: model.whitelistedPitchClasses
|
2020-04-13 00:35:16 +02:00
|
|
|
in
|
|
|
|
( { model
|
2020-04-13 11:02:03 +02:00
|
|
|
| whitelistedPitchClasses = pitchClasses
|
2020-04-13 00:35:16 +02:00
|
|
|
, whitelistedChords =
|
|
|
|
Theory.allChords
|
|
|
|
{ start = model.firstNote
|
|
|
|
, end = model.lastNote
|
|
|
|
, inversions = model.whitelistedInversions
|
|
|
|
, chordTypes = model.whitelistedChordTypes
|
2020-04-13 11:02:03 +02:00
|
|
|
, pitchClasses = pitchClasses
|
2020-04-12 20:32:54 +02:00
|
|
|
}
|
2020-04-12 20:20:00 +02:00
|
|
|
}
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
2020-04-13 16:07:03 +02:00
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2020-04-12 17:43:34 +02:00
|
|
|
SetTempo tempo ->
|
|
|
|
( { model
|
|
|
|
| tempo =
|
|
|
|
case String.toInt tempo of
|
|
|
|
Just x ->
|
|
|
|
x
|
|
|
|
|
|
|
|
Nothing ->
|
|
|
|
model.tempo
|
|
|
|
}
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
2020-04-11 17:09:11 +02:00
|
|
|
|
|
|
|
playPause : Model -> Html Msg
|
2020-04-12 17:43:34 +02:00
|
|
|
playPause { isPaused } =
|
|
|
|
if isPaused then
|
|
|
|
button [ onClick Play ] [ text "Play" ]
|
|
|
|
|
|
|
|
else
|
|
|
|
button [ onClick Pause ] [ text "Pause" ]
|
|
|
|
|
|
|
|
|
2020-04-12 20:32:54 +02:00
|
|
|
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)
|
|
|
|
]
|
|
|
|
[]
|
|
|
|
]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-04-12 20:20:00 +02:00
|
|
|
inversionCheckboxes : List Theory.ChordInversion -> Html Msg
|
|
|
|
inversionCheckboxes inversions =
|
|
|
|
ul []
|
|
|
|
(Theory.allInversions
|
|
|
|
|> List.map
|
|
|
|
(\inversion ->
|
|
|
|
li []
|
|
|
|
[ label [] [ text (Theory.inversionName inversion) ]
|
|
|
|
, input
|
2020-04-13 00:35:16 +02:00
|
|
|
[ type_ "checkbox"
|
2020-04-12 20:20:00 +02:00
|
|
|
, onClick (ToggleInversion inversion)
|
|
|
|
, checked (List.member inversion inversions)
|
|
|
|
]
|
|
|
|
[]
|
|
|
|
]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-04-13 23:39:15 +02:00
|
|
|
selectKey :
|
|
|
|
Model
|
|
|
|
->
|
2020-04-17 14:35:33 +02:00
|
|
|
{ relativeMajor : Theory.Key
|
|
|
|
, relativeMinor : Theory.Key
|
2020-04-13 23:39:15 +02:00
|
|
|
}
|
|
|
|
-> Html Msg
|
2020-04-17 14:35:33 +02:00
|
|
|
selectKey model { relativeMajor, relativeMinor } =
|
2020-04-13 23:39:15 +02:00
|
|
|
let
|
|
|
|
active key =
|
|
|
|
List.member key model.whitelistedKeys
|
2020-04-17 14:35:33 +02:00
|
|
|
|
|
|
|
buttonLabel major minor =
|
|
|
|
Theory.viewKey major ++ ", " ++ Theory.viewKey minor
|
2020-04-13 23:39:15 +02:00
|
|
|
in
|
|
|
|
div [ class "flex pt-0" ]
|
2020-04-17 14:35:33 +02:00
|
|
|
[ UI.textToggleButton
|
|
|
|
{ label = buttonLabel relativeMajor relativeMinor
|
|
|
|
, handleClick = ToggleKey relativeMinor
|
2020-04-13 23:39:15 +02:00
|
|
|
, classes = [ "flex-1" ]
|
2020-04-18 15:24:41 +02:00
|
|
|
, toggled = active relativeMinor || active relativeMajor
|
2020-04-13 23:39:15 +02:00
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
keyCheckboxes : Model -> Html Msg
|
|
|
|
keyCheckboxes model =
|
2020-04-17 14:35:33 +02:00
|
|
|
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
|
2020-04-13 16:07:03 +02:00
|
|
|
div []
|
2020-04-17 14:35:33 +02:00
|
|
|
[ h2 [ class "text-gray-500 text-center pt-10 text-5xl" ] [ text "Select keys" ]
|
2020-04-13 16:07:03 +02:00
|
|
|
, ul []
|
2020-04-17 14:35:33 +02:00
|
|
|
(circleOfFifths
|
2020-04-13 16:07:03 +02:00
|
|
|
|> List.map
|
2020-04-17 14:35:33 +02:00
|
|
|
(\( major, minor ) ->
|
2020-04-13 23:39:15 +02:00
|
|
|
selectKey model
|
2020-04-17 14:35:33 +02:00
|
|
|
{ relativeMajor = majorKey major
|
|
|
|
, relativeMinor = minorKey minor
|
2020-04-13 23:39:15 +02:00
|
|
|
}
|
2020-04-13 16:07:03 +02:00
|
|
|
)
|
|
|
|
)
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2020-04-13 23:39:15 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2020-04-17 13:38:08 +02:00
|
|
|
openPreferences : Html Msg
|
|
|
|
openPreferences =
|
|
|
|
button
|
2020-04-18 15:24:41 +02:00
|
|
|
[ class "w-48 h-48 absolute left-0 top-0 z-40"
|
2020-04-17 13:38:08 +02:00
|
|
|
, 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 ]
|
|
|
|
|
|
|
|
|
2020-04-13 23:39:15 +02:00
|
|
|
preferences : Model -> Html Msg
|
|
|
|
preferences model =
|
|
|
|
div [ class "pt-10 pb-20 px-10" ]
|
2020-04-17 13:38:08 +02:00
|
|
|
[ closePreferences
|
|
|
|
, Tempo.render
|
2020-04-13 10:42:26 +02:00
|
|
|
{ tempo = model.tempo
|
|
|
|
, handleInput = SetTempo
|
|
|
|
}
|
2020-04-13 16:07:03 +02:00
|
|
|
, case model.practiceMode of
|
|
|
|
KeyMode ->
|
2020-04-13 23:39:15 +02:00
|
|
|
keyCheckboxes model
|
2020-04-13 16:07:03 +02:00
|
|
|
|
|
|
|
FineTuneMode ->
|
|
|
|
div []
|
2020-04-13 23:39:15 +02:00
|
|
|
[ inversionCheckboxes model.whitelistedInversions
|
2020-04-13 16:07:03 +02:00
|
|
|
, chordTypeCheckboxes model.whitelistedChordTypes
|
|
|
|
]
|
2020-04-13 23:39:15 +02:00
|
|
|
]
|
2020-04-13 10:42:26 +02:00
|
|
|
|
2020-04-13 23:39:15 +02:00
|
|
|
|
|
|
|
practice : Model -> Html Msg
|
|
|
|
practice model =
|
2020-04-17 13:38:08 +02:00
|
|
|
let
|
2020-04-18 15:24:41 +02:00
|
|
|
( handleClick, buttonText ) =
|
2020-04-17 13:38:08 +02:00
|
|
|
if model.isPaused then
|
2020-04-18 15:24:41 +02:00
|
|
|
( Play, "Press to practice" )
|
2020-04-17 13:38:08 +02:00
|
|
|
|
|
|
|
else
|
2020-04-18 15:24:41 +02:00
|
|
|
( Pause, "" )
|
2020-04-17 13:38:08 +02:00
|
|
|
in
|
|
|
|
div []
|
2020-04-18 15:24:41 +02:00
|
|
|
[ openPreferences
|
|
|
|
, UI.overlayButton
|
|
|
|
{ label = buttonText
|
|
|
|
, handleClick = handleClick
|
|
|
|
, isVisible = model.isPaused
|
|
|
|
}
|
2020-04-17 13:38:08 +02:00
|
|
|
, Piano.render
|
|
|
|
{ highlight = model.selectedChord |> Maybe.andThen Theory.notesForChord |> Maybe.withDefault []
|
|
|
|
, start = model.firstNote
|
|
|
|
, end = model.lastNote
|
|
|
|
}
|
2020-04-13 10:42:26 +02:00
|
|
|
]
|
2020-04-12 17:43:34 +02:00
|
|
|
|
|
|
|
|
2020-04-13 23:39:15 +02:00
|
|
|
view : Model -> Html Msg
|
|
|
|
view model =
|
|
|
|
case model.view of
|
|
|
|
Preferences ->
|
|
|
|
preferences model
|
|
|
|
|
|
|
|
Practice ->
|
|
|
|
practice model
|
|
|
|
|
|
|
|
|
2020-04-12 17:43:34 +02:00
|
|
|
{-| For now, I'm just dumping things onto the page to sketch ideas.
|
|
|
|
-}
|
2020-04-11 00:03:01 +02:00
|
|
|
main =
|
2020-04-12 17:43:34 +02:00
|
|
|
Browser.element
|
|
|
|
{ init = \() -> ( init, Cmd.none )
|
|
|
|
, subscriptions = subscriptions
|
|
|
|
, update = update
|
|
|
|
, view = view
|
|
|
|
}
|