diff --git a/client/src/MassiveDecks/Components/TTS.elm b/client/src/MassiveDecks/Components/TTS.elm new file mode 100644 index 0000000..dd86dfc --- /dev/null +++ b/client/src/MassiveDecks/Components/TTS.elm @@ -0,0 +1,30 @@ +port module MassiveDecks.Components.TTS exposing (Model, Message(..), update, init) + +import MassiveDecks.Util as Util + + +type alias Model = + { enabled : Bool + } + + +type Message + = Say String + | Enabled Bool + + +init : Model +init = { enabled = False } + + +update : Message -> Model -> (Model, Cmd Message) +update message model = + case message of + Say text -> + (model, if model.enabled then say text else Cmd.none) + + Enabled enabled -> + ({ model | enabled = enabled }, if enabled then Cmd.none else Say "" |> Util.cmd) + + +port say : String -> Cmd msg diff --git a/client/src/MassiveDecks/Scenes/Lobby.elm b/client/src/MassiveDecks/Scenes/Lobby.elm index 1f9ca93..14ed1eb 100644 --- a/client/src/MassiveDecks/Scenes/Lobby.elm +++ b/client/src/MassiveDecks/Scenes/Lobby.elm @@ -15,6 +15,7 @@ import MassiveDecks.Components.Errors as Errors import MassiveDecks.Components.QR as QR import MassiveDecks.Components.BrowserNotifications as BrowserNotifications import MassiveDecks.Components.Overlay as Overlay exposing (Overlay) +import MassiveDecks.Components.TTS as TTS import MassiveDecks.Models exposing (Init) import MassiveDecks.Models.Event as Event exposing (Event) import MassiveDecks.Models.JSON.Encode exposing (encodePlayerSecret) @@ -47,6 +48,7 @@ init init lobbyAndHand secret = , notification = Nothing , qrNeedsRendering = False , sidebar = Sidebar.init 768 {- Should match the enhance-width in less. -} + , tts = TTS.init } ! [] @@ -142,6 +144,9 @@ update message model = Playing.HandUpdate hand -> model |> updateLobbyAndHand (Game.LobbyAndHand model.lobby hand) + Playing.TTSMessage ttsMessage -> + (model, TTSMessage ttsMessage |> LocalMessage |> Util.cmd) + Playing.LocalMessage localMessage -> let (playing, cmd) = Playing.update localMessage model @@ -154,6 +159,12 @@ update message model = in ({ model | sidebar = sidebarModel }, Cmd.map (LocalMessage << SidebarMessage) cmd) + TTSMessage ttsMessage -> + let + (ttsModel, cmd) = TTS.update ttsMessage model.tts + in + ({ model | tts = ttsModel }, Cmd.map (LocalMessage << TTSMessage) cmd) + BrowserNotificationsMessage notificationMessage -> let (browserNotifications, localCmd, cmd) = BrowserNotifications.update notificationMessage model.browserNotifications diff --git a/client/src/MassiveDecks/Scenes/Lobby/Messages.elm b/client/src/MassiveDecks/Scenes/Lobby/Messages.elm index a692b7f..e44e663 100644 --- a/client/src/MassiveDecks/Scenes/Lobby/Messages.elm +++ b/client/src/MassiveDecks/Scenes/Lobby/Messages.elm @@ -10,6 +10,7 @@ import MassiveDecks.Scenes.Playing.Messages as Playing import MassiveDecks.Components.Errors as Errors import MassiveDecks.Components.Overlay as Overlay import MassiveDecks.Components.BrowserNotifications as BrowserNotifications +import MassiveDecks.Components.TTS as TTS {-| This type is used for all sending of messages, allowing us to send messages handled outside this scene. @@ -39,3 +40,4 @@ type Message | ConfigMessage Config.ConsumerMessage | PlayingMessage Playing.ConsumerMessage | SidebarMessage Sidebar.Message + | TTSMessage TTS.Message diff --git a/client/src/MassiveDecks/Scenes/Lobby/Models.elm b/client/src/MassiveDecks/Scenes/Lobby/Models.elm index bb022a3..f9e3de7 100644 --- a/client/src/MassiveDecks/Scenes/Lobby/Models.elm +++ b/client/src/MassiveDecks/Scenes/Lobby/Models.elm @@ -9,6 +9,7 @@ import MassiveDecks.Scenes.Lobby.Sidebar as Sidebar import MassiveDecks.Scenes.Config.Models as Config import MassiveDecks.Scenes.Playing.Models as Playing import MassiveDecks.Components.BrowserNotifications as BrowserNotifications +import MassiveDecks.Components.TTS as TTS {-| The state of the lobby. @@ -24,4 +25,5 @@ type alias Model = , notification : Maybe Notification , qrNeedsRendering : Bool , sidebar : Sidebar.Model + , tts : TTS.Model } diff --git a/client/src/MassiveDecks/Scenes/Lobby/UI.elm b/client/src/MassiveDecks/Scenes/Lobby/UI.elm index 9b37ca3..b45bc49 100644 --- a/client/src/MassiveDecks/Scenes/Lobby/UI.elm +++ b/client/src/MassiveDecks/Scenes/Lobby/UI.elm @@ -11,6 +11,7 @@ import MassiveDecks.Components.QR as QR import MassiveDecks.Components.Icon as Icon import MassiveDecks.Components.Overlay as Overlay import MassiveDecks.Components.BrowserNotifications as BrowserNotifications +import MassiveDecks.Components.TTS as TTS import MassiveDecks.Scenes.Config as Config import MassiveDecks.Scenes.Playing as Playing import MassiveDecks.Scenes.Lobby.Models exposing (Model) @@ -252,6 +253,14 @@ gameMenu model = ] [ Icon.fwIcon "bullhorn", text " Invite Players" ] ] ] ++ (notificationsMenuItem model.browserNotifications) ++ [ li [] [ a [ class "link" + , attribute "tabindex" "0" + , attribute "role" "button" + , onClick (TTS.Enabled (not model.tts.enabled) |> TTSMessage |> LocalMessage) + ] + (if model.tts.enabled then [ Icon.fwIcon "volume-off", text " Disable Speech" ] + else [ Icon.fwIcon "volume-up", text " Enable Speech" ]) + ] + , li [] [ a [ class "link" , attribute "tabindex" "0" , attribute "role" "button" , onClick Leave diff --git a/client/src/MassiveDecks/Scenes/Playing.elm b/client/src/MassiveDecks/Scenes/Playing.elm index d967a2b..43c783a 100644 --- a/client/src/MassiveDecks/Scenes/Playing.elm +++ b/client/src/MassiveDecks/Scenes/Playing.elm @@ -12,11 +12,13 @@ import MassiveDecks.API as API import MassiveDecks.API.Request as Request import MassiveDecks.Models exposing (Init) import MassiveDecks.Components.Errors as Errors +import MassiveDecks.Components.TTS as TTS import MassiveDecks.Models.Card as Card import MassiveDecks.Scenes.Lobby.Models as Lobby import MassiveDecks.Scenes.History as History import MassiveDecks.Scenes.History.Messages as History import MassiveDecks.Scenes.Playing.UI as UI +import MassiveDecks.Scenes.Playing.UI.Cards as CardsUI import MassiveDecks.Scenes.Playing.Models exposing (Model, ShownPlayedCards, ShownCard) import MassiveDecks.Scenes.Playing.Messages exposing (ConsumerMessage(..), Message(..)) import MassiveDecks.Scenes.Playing.HouseRule as HouseRule exposing (HouseRule) @@ -105,9 +107,20 @@ update message lobbyModel = ) Consider potentialWinnerIndex -> - ( { model | considering = Just potentialWinnerIndex } - , Cmd.none - ) + let + speech = + lobby.round `Maybe.andThen` (\round -> + case round.responses of + Card.Revealed responses -> + Util.get responses.cards potentialWinnerIndex |> Maybe.map (\fill -> (round, fill)) + Card.Hidden _ -> + Nothing) + |> Maybe.map (\(round, callFill) -> TTS.Say (CardsUI.callText round.call callFill) |> TTSMessage |> Util.cmd) + |> Maybe.withDefault Cmd.none + in + ( { model | considering = Just potentialWinnerIndex } + , speech + ) Choose winnerIndex -> ( { model | considering = Nothing } @@ -144,7 +157,12 @@ update message lobbyModel = (model, Request.send (API.redraw lobbyModel.lobby.gameCode lobbyModel.secret) redrawErrorHandler ErrorMessage HandUpdate) FinishRound finishedRound -> - ({ model | finishedRound = Just finishedRound}, Cmd.none) + let + cards = finishedRound.responses + winning = Card.winningCards cards finishedRound.playedByAndWinner |> Maybe.withDefault [] + speech = Card.filled finishedRound.call winning + in + ({ model | finishedRound = Just finishedRound}, TTS.Say speech |> TTSMessage |> Util.cmd) HistoryMessage historyMessage -> case model.history of diff --git a/client/src/MassiveDecks/Scenes/Playing/Messages.elm b/client/src/MassiveDecks/Scenes/Playing/Messages.elm index 589594b..13f70a0 100644 --- a/client/src/MassiveDecks/Scenes/Playing/Messages.elm +++ b/client/src/MassiveDecks/Scenes/Playing/Messages.elm @@ -1,6 +1,7 @@ module MassiveDecks.Scenes.Playing.Messages exposing (..) import MassiveDecks.Components.Errors as Errors +import MassiveDecks.Components.TTS as TTS import MassiveDecks.Scenes.History.Messages as History import MassiveDecks.Models.Player as Player import MassiveDecks.Models.Game as Game @@ -11,6 +12,7 @@ import MassiveDecks.Models.Card as Card -} type ConsumerMessage = HandUpdate Card.Hand + | TTSMessage TTS.Message | ErrorMessage Errors.Message | LocalMessage Message diff --git a/client/src/MassiveDecks/Scenes/Playing/UI/Cards.elm b/client/src/MassiveDecks/Scenes/Playing/UI/Cards.elm index 835c9c1..1032fe0 100644 --- a/client/src/MassiveDecks/Scenes/Playing/UI/Cards.elm +++ b/client/src/MassiveDecks/Scenes/Playing/UI/Cards.elm @@ -1,4 +1,6 @@ -module MassiveDecks.Scenes.Playing.UI.Cards exposing (call, response) +module MassiveDecks.Scenes.Playing.UI.Cards exposing (call, callText, response) + +import String import Html exposing (..) import Html.Attributes exposing (..) @@ -24,6 +26,21 @@ call call picked = div [ class "card call mui-panel" ] [ div [ class "call-text" ] callContents ] +callText : Card.Call -> List Card.Response -> String +callText call picked = + let + responseFirst = call.parts |> List.head |> Maybe.map ((==) "") |> Maybe.withDefault False + pickedText = List.map .text picked + (parts, responses) = if responseFirst then + (call.parts, Util.mapFirst Util.firstLetterToUpper pickedText) + else + (Util.mapFirst Util.firstLetterToUpper call.parts, pickedText) + extra = (Card.slots call) - List.length picked + withSlots = Util.interleave (List.concat [pickedText, List.repeat extra "blank"]) call.parts + in + String.join " " withSlots + + slot : String -> Html msg slot value = (span [ class "slot" ] [ text value ]) diff --git a/public/javascripts/util.js b/public/javascripts/util.js index 2295cd9..acf5dde 100644 --- a/public/javascripts/util.js +++ b/public/javascripts/util.js @@ -33,6 +33,15 @@ function start(url, version) { } }); + if ('speechSynthesis' in window) { + game.ports.say.subscribe(function (text) { + window.speechSynthesis.cancel(); + var utterance = new SpeechSynthesisUtterance(text); + utterance.lang = "en-GB"; + window.speechSynthesis.speak(utterance); + }); + } + game.ports.qr.subscribe(function (idAndValue) { new QRCode(document.getElementById(idAndValue.id), { text: idAndValue.value,