{- - This file is part of `typers`. - - `typers` is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - `typers` is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero Public License for more details. - - You should have received a copy of the GNU Affero Public License - along with `typers`. If not, see . -} module Main exposing (..) import Array exposing (..) import Base exposing (..) import Browser import Css import Css.Global exposing (body, everything, global, selector) import Data exposing (..) import Html import Html.Styled exposing (..) import Html.Styled.Attributes exposing (..) import Html.Styled.Events exposing (onClick, onInput) import Random import Styles exposing (styledButton, styledInput, styledTextBox, wordStyle) import Task import Time import Utils exposing (flip, isJust, isNothing) import Views exposing (..) main = Browser.document { init = init , view = \m -> { title = "typers", body = [ view m |> toUnstyled ] } , update = update , subscriptions = subscriptions } subscriptions : Model -> Sub Msg subscriptions model = Sub.none stringsToWords : List String -> List Word stringsToWords ls = List.map (flip Word <| Todo) ls generateWords : Config -> Cmd Msg generateWords config = Random.generate LoadWords (randomParagraph config) defaultModel : Model defaultModel = Model Nothing Nothing Array.empty Nothing defaultConfig 0 "" False init : () -> ( Model, Cmd Msg ) init _ = ( defaultModel , generateWords defaultConfig ) setStartTime : Cmd Msg setStartTime = Task.perform Started Time.now setEndTime : Cmd Msg setEndTime = Task.perform Finished Time.now firstWord : Model -> Bool firstWord model = String.length model.inputBox == 1 && model.currentWord == 0 lastWord : Model -> Bool lastWord model = model.currentWord == model.config.length - 1 wordEnded : String -> Bool wordEnded s = String.endsWith " " s handleWordEnded : Model -> ( Model, Cmd Msg ) handleWordEnded model = let s = model.inputBox current = Array.get model.currentWord model.words |> Maybe.withDefault (Word "" Todo) currentContent = current.content prevWordStatus = if s == currentContent then Correct else Wrong newWords = Array.set model.currentWord { current | status = prevWordStatus } model.words newModel = { model | inputBox = "", words = newWords, currentWord = model.currentWord + 1 } cmd = if lastWord model then setEndTime else Cmd.none in ( newModel, cmd ) update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of Started t -> ( { model | begin = Just t } , Cmd.none ) Finished t -> ( { model | end = Just t } , Cmd.none ) LoadWords ls -> ( { model | words = Array.fromList <| stringsToWords ls } , Cmd.none ) InputChanged s -> let cmd = if firstWord model && isNothing model.begin then setStartTime else if s == currentContents model && lastWord model then setEndTime else Cmd.none inpStat = String.startsWith s (currentContents model) in ( { model | inputBox = s, inputCorrect = inpStat } , cmd ) WordLengthChanged n -> let oldConfig = model.config newConfig = { oldConfig | length = n } in ( { defaultModel | config = newConfig } , generateWords newConfig ) Redo -> let oldConfig = model.config newModel = { defaultModel | config = oldConfig } in ( newModel, generateWords oldConfig ) NextWord -> if isJust model.end then ( model, Cmd.none ) else handleWordEnded model _ -> ( model , Cmd.none ) toPara : Array Word -> Int -> String -> Html Msg toPara words current content = words |> Array.map (\w -> span [ css [ wordStyle w.status ] ] [ text w.content ]) |> Array.set current (span [ css [ wordStyle CurrentWord ] ] [ text content ]) |> Array.toList |> List.intersperse (span [] [ text " " ]) |> styledTextBox [] currentContents : Model -> String currentContents model = Array.get model.currentWord model.words |> Maybe.map .content |> Maybe.withDefault "" view : Model -> Html Msg view model = div [] [ div [ style "width" "95%" , style "max-width" "650px" , style "margin" "15% auto auto auto" ] [ div [ style "width" "100%" ] [ viewWordLengthOptions , div [ style "float" "right" ] [ styledButton [ onClick Redo ] [ text "redo" ] ] ] , div [] [ toPara model.words model.currentWord (currentContents model) , div [] [ text "> " , styledInput model.inputCorrect [ onInput handleInputChanged, value model.inputBox ] [] ] ] , div [] [ viewStats model ] , global [ selector "li:not(:last-child)::after" [ Css.property "content" "' ยท '" ] ] , global [ selector "*,*:focus,*:hover" [ Css.property "outline" "none" ] ] , global [ everything [ Css.fontFamily Css.monospace, Css.fontSize (Css.px 18) ] ] ] , div [ style "position" "absolute" , style "bottom" "0" , style "left" "50%" , style "transform" "translate(-50%)" ] [ viewFooter ] ] handleInputChanged : String -> Msg handleInputChanged s = if wordEnded s then NextWord else InputChanged s