Resolve #117: Move from Weightless to MWC.

Resolve #132: Fix some issues with the Postgres backend.
This commit is contained in:
Gareth Latty
2020-05-16 15:53:43 +01:00
parent 137a2a9f9e
commit 749485b16a
188 changed files with 10533 additions and 6776 deletions
+1 -1
View File
@@ -18,6 +18,7 @@
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm-community/dict-extra": "2.4.0",
"elm-community/list-extra": "8.2.3",
"elm-community/random-extra": "3.1.0",
"fapian/elm-html-aria": "1.4.0",
"lattyware/elm-fontawesome": "4.0.0",
@@ -31,7 +32,6 @@
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/virtual-dom": "1.0.2",
"elm-community/list-extra": "8.2.3",
"owanturist/elm-union-find": "1.0.0"
}
},
+3003 -1628
View File
File diff suppressed because it is too large Load Diff
+25 -8
View File
@@ -23,16 +23,32 @@
],
"dependencies": {
"@fortawesome/fontawesome-free": "^5.12.1",
"@material/card": "^6.0.0",
"@material/mwc-button": "^0.15.0",
"@material/mwc-fab": "^0.15.0",
"@material/mwc-icon-button": "^0.15.0",
"@material/mwc-linear-progress": "^0.15.0",
"@material/mwc-menu": "^0.15.0",
"@material/mwc-select": "^0.15.0",
"@material/mwc-slider": "^0.15.0",
"@material/mwc-switch": "^0.15.0",
"@material/mwc-tab": "^0.15.0",
"@material/mwc-tab-bar": "^0.15.0",
"@material/mwc-textarea": "^0.15.0",
"@material/mwc-textfield": "^0.15.0",
"@polymer/paper-tooltip": "^3.0.1",
"@webcomponents/webcomponentsjs": "^2.4.3",
"weightless": "0.0.37"
"canvas-confetti": "^1.2.0"
},
"devDependencies": {
"@types/chrome": "^0.0.106",
"@types/canvas-confetti": "^1.0.0",
"@types/chrome": "^0.0.107",
"@types/chromecast-caf-receiver": "^5.0.4",
"@types/chromecast-caf-sender": "^1.0.3",
"app-manifest-loader": "^2.4.1",
"chromecast-device-emulator": "^1.2.7",
"clean-webpack-plugin": "^3.0.0",
"closure-webpack-plugin": "^2.3.0",
"compression-webpack-plugin": "^3.1.0",
"css-hot-loader": "^1.4.4",
"css-loader": "^3.5.3",
@@ -41,24 +57,25 @@
"elm-hot-webpack-loader": "^1.1.6",
"elm-webpack-loader": "^6.0.1",
"extract-loader": "^5.0.1",
"fibers": "^5.0.0",
"file-loader": "^6.0.0",
"google-closure-compiler": "^20200504.0.0",
"html-loader": "^1.1.0",
"html-webpack-plugin": "^4.3.0",
"node-sass": "^4.14.0",
"postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"prettier": "2.0.5",
"prettier-plugin-elm": "^0.7.0",
"sass-inline-svg": "^1.2.3",
"resolve-url-loader": "^3.1.1",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"style-loader": "^1.2.1",
"terser-webpack-plugin": "^3.0.0",
"ts-loader": "^7.0.2",
"terser-webpack-plugin": "^3.0.1",
"ts-loader": "^7.0.3",
"typescript": "^3.8.3",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
"webpack-dev-server": "^3.11.0"
}
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
plugins: {
"postcss-import": {},
"postcss-preset-env": {},
cssnano: {},
},
};
+3 -2
View File
@@ -15,6 +15,7 @@ import MassiveDecks.Error.Overlay as Overlay
import MassiveDecks.Messages exposing (..)
import MassiveDecks.Model exposing (..)
import MassiveDecks.Models.Decoders as Decoders
import MassiveDecks.Models.MdError as MdError
import MassiveDecks.Notifications as Notifications
import MassiveDecks.Pages as Pages
import MassiveDecks.Pages.Loading as Loading
@@ -377,10 +378,10 @@ handleLobbyMsg baseModel lobbyMsg lobbyModel =
Lobby.Stay newLobbyModel ->
( { model | page = Pages.Lobby newLobbyModel }, lobbyCmd )
Lobby.AuthError gc authenticationError ->
Lobby.JoinError gc error ->
let
( page, changePageCmd ) =
Start.initWithAuthError shared gc authenticationError
Start.initWithError shared gc error
urlCommand =
page |> .route |> Route.Start |> Route.url |> Navigation.pushUrl shared.key
+78 -51
View File
@@ -16,7 +16,6 @@ module MassiveDecks.Card.Source exposing
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import MassiveDecks.Card.Source.BuiltIn as BuiltIn
import MassiveDecks.Card.Source.Cardcast as Cardcast
import MassiveDecks.Card.Source.Custom as Player
@@ -29,8 +28,8 @@ import MassiveDecks.Pages.Lobby.Configure.Decks.Model exposing (DeckOrError)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Maybe as Maybe
import Weightless as Wl
import Weightless.Attributes as WlA
import Material.Select as Select
import Paper.Tooltip as Tooltip
{-| The default source for an editor.
@@ -61,24 +60,28 @@ externalAndEquals a b =
{-| Get an general methods of the given type.
-}
generalMethods : String -> Maybe (ExternalGeneralMethods msg)
generalMethods n =
case n of
"BuiltIn" ->
BuiltIn.generalMethods |> Just
generalMethods : General -> ExternalGeneralMethods msg
generalMethods source =
case source of
GBuiltIn ->
BuiltIn.generalMethods
"Cardcast" ->
Cardcast.generalMethods |> Just
_ ->
Nothing
GCardcast ->
Cardcast.generalMethods
{-| Get an empty source of the given type.
-}
empty : Shared -> String -> Maybe External
empty shared n =
generalMethods n |> Maybe.map (\m -> m.empty shared)
empty : Shared -> General -> External
empty shared =
generalMethods >> (\m -> m.empty shared)
{-| Get the general source for the given specific source.
-}
generalMatching : External -> General
generalMatching source =
(externalMethods source |> .id) ()
{-| An empty source of the same general type as the given one.
@@ -111,28 +114,46 @@ defaultDetails shared source =
{-| A tooltip for a source.
-}
tooltip : Shared -> Source -> Maybe ( String, Html msg )
tooltip shared source =
case shared |> (methods source |> .tooltip) of
Just ( id, rendered ) ->
Just
( id
, Wl.tooltip
(List.concat
[ [ WlA.anchor id
, WlA.fixed
, WlA.anchorOpenEvents [ "mouseover" ]
, WlA.anchorCloseEvents [ "mouseout" ]
]
, WlA.anchorOrigin WlA.XRight WlA.YCenter
, WlA.transformOrigin WlA.XLeft WlA.YCenter
]
)
[ rendered ]
)
tooltip : Shared -> Tooltip.Position -> Details -> Source -> Maybe ( String, Html msg )
tooltip shared position details source =
let
ms =
methods source
in
ms.tooltip (generalTooltip shared position details)
Nothing ->
Nothing
{-| A general tooltip for any source.
-}
generalTooltip : Shared -> Tooltip.Position -> Details -> String -> List (Html msg) -> Html msg
generalTooltip shared position details forId sourceSpecificContent =
let
lang l =
Html.p [ HtmlA.class "language" ]
[ Strings.DeckLanguage { language = l |> Lang.languageName } |> Lang.html shared
]
author a =
Html.p [ HtmlA.class "author" ] [ Strings.DeckAuthor { author = a } |> Lang.html shared ]
translator t =
Html.p [ HtmlA.class "translator" ] [ Strings.DeckTranslator { translator = t } |> Lang.html shared ]
generalContent =
[ details.language |> Maybe.andThen (\l -> Maybe.justIf (l /= Lang.currentLanguage shared) (lang l))
, details.author |> Maybe.map author
, details.translator |> Maybe.map translator
]
|> List.filterMap identity
in
[ Html.div [ HtmlA.class "source-tooltip" ]
([ sourceSpecificContent
, generalContent
]
|> List.concat
)
]
|> Tooltip.view position forId
{-| The logo for a source.
@@ -144,32 +165,38 @@ logo source =
{-| An editor for any supported external source.
-}
generalEditor : Shared -> List DeckOrError -> External -> (External -> msg) -> List (Html msg)
generalEditor shared existing currentValue update =
generalEditor : Shared -> List DeckOrError -> External -> (External -> msg) -> Maybe msg -> msg -> List (Html msg)
generalEditor shared existing currentValue update submit noOp =
let
enabledSources =
[ shared.sources.builtIn |> Maybe.map (\_ -> BuiltIn.generalMethods)
, Cardcast.generalMethods |> Maybe.justIf shared.sources.cardcast
]
toOption source =
Html.option [ HtmlA.value (source.id ()) ]
[ () |> source.name |> Lang.html shared
]
toItem source =
{ id = source.id ()
, icon = source.logo ()
, primary = [ () |> source.name |> Lang.html shared ]
, secondary = Nothing
, meta = Nothing
}
in
[ Wl.select
[ HtmlA.id "source-selector"
, WlA.outlined
, HtmlE.onInput (empty shared >> Maybe.withDefault (default shared) >> update)
]
(enabledSources |> List.filterMap (Maybe.map toOption))
, editor shared existing currentValue update
[ Select.view shared
{ label = Strings.DeckSource
, idToString = generalToString
, idFromString = generalFromString
, selected = currentValue |> generalMatching |> Just
, wrap = Maybe.map (empty shared) >> Maybe.withDefault (default shared) >> update
}
[ HtmlA.id "source-selector" ]
(enabledSources |> List.filterMap (Maybe.map toItem))
, editor shared existing currentValue update submit noOp
]
{-| An editor for the given source value.
-}
editor : Shared -> List DeckOrError -> External -> (External -> msg) -> Html msg
editor : Shared -> List DeckOrError -> External -> (External -> msg) -> Maybe msg -> msg -> Html msg
editor shared existing source =
(externalMethods source |> .editor) shared existing
@@ -3,27 +3,32 @@ module MassiveDecks.Card.Source.BuiltIn exposing
, methods
)
import FontAwesome.Icon as Icon
import FontAwesome.Solid as Icon
import Html as Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import MassiveDecks.Card.Source.BuiltIn.Model exposing (..)
import MassiveDecks.Card.Source.Methods as Source
import MassiveDecks.Card.Source.Model as Source exposing (Source)
import MassiveDecks.Components.Form.Message exposing (Message)
import MassiveDecks.Icon as Icon
import MassiveDecks.Model exposing (..)
import MassiveDecks.Pages.Lobby.Configure.Decks.Model as Decks exposing (DeckOrError)
import MassiveDecks.Pages.Lobby.Configure.Decks.Model exposing (DeckOrError)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang exposing (sortClosestFirst)
import MassiveDecks.Util.Html as Html
import Weightless as Wl
import Weightless.Attributes as WlA
import MassiveDecks.Util.Maybe as Maybe
import MassiveDecks.Util.NeList as NonEmptyList
import MassiveDecks.Util.Order as Order
import Material.Select as Select
methods : Id -> Source.ExternalMethods msg
methods given =
{ name = name
{ name = sourceName
, logo = logo
, empty = empty
, id = id
, id = sourceId
, problems = problems given
, defaultDetails = details given
, tooltip = tooltip given
@@ -34,10 +39,10 @@ methods given =
generalMethods : Source.ExternalGeneralMethods msg
generalMethods =
{ name = name
{ name = sourceName
, logo = logo
, empty = empty
, id = id
, id = sourceId
}
@@ -45,29 +50,31 @@ generalMethods =
{- Private -}
id : () -> String
id _ =
"BuiltIn"
sourceId : () -> Source.General
sourceId _ =
Source.GBuiltIn
name : () -> MdString
name () =
sourceName : () -> MdString
sourceName () =
Strings.BuiltIn
empty : Shared -> Source.External
empty shared =
shared.sources.builtIn
|> Maybe.andThen (.decks >> List.head)
|> Maybe.map (.decks >> NonEmptyList.head)
|> Maybe.map .id
|> Maybe.withDefault (Id "")
|> Maybe.withDefault (hardcoded "")
|> Source.BuiltIn
{-| See if the id is equal to the given source.
-}
equals : Id -> Source.External -> Bool
equals (Id given) source =
equals given source =
case source of
Source.BuiltIn (Id other) ->
Source.BuiltIn other ->
given == other
_ ->
@@ -79,27 +86,57 @@ problems _ () =
[]
editor : Id -> Shared -> List DeckOrError -> (Source.External -> msg) -> Html msg
editor (Id selectedId) shared _ update =
editor : Id -> Shared -> List DeckOrError -> (Source.External -> msg) -> Maybe msg -> msg -> Html msg
editor selected shared existing update _ _ =
case shared.sources.builtIn of
Just { decks } ->
let
deck deckInfo =
case deckInfo.id of
Id other ->
Html.option
[ HtmlA.selected (selectedId == other)
, HtmlA.value other
]
[ Html.text deckInfo.name ]
deck { id, name, language, author, translator } =
let
lang =
if language /= Lang.currentLanguage shared then
language |> Lang.languageName |> Just
else
Nothing
matches d =
case d.source of
Source.BuiltIn other ->
other == id
_ ->
False
secondary =
[ lang |> Maybe.map (\l -> Strings.DeckLanguage { language = l } |> Lang.html shared)
, Strings.DeckAuthor { author = author } |> Lang.html shared |> Just
, translator |> Maybe.map (\t -> Strings.DeckTranslator { translator = t } |> Lang.html shared)
]
|> List.filterMap identity
|> List.intersperse (Html.text ", ")
in
{ id = id
, icon = Nothing
, primary = [ Html.text name ]
, secondary = Just secondary
, meta = Icon.check |> Icon.viewIcon |> Maybe.justIf (existing |> List.any matches)
}
in
Html.div [ HtmlA.class "primary" ]
[ Wl.select
[ HtmlA.id "built-in-selector"
, WlA.outlined
, Id >> Source.BuiltIn >> update |> HtmlE.onInput
]
(decks |> List.map deck)
[ Select.view shared
{ label = Strings.Deck
, idToString = toString
, idFromString = fromString shared.sources.builtIn
, selected = Just selected
, wrap = Maybe.withDefault (hardcoded "") >> Source.BuiltIn >> update
}
[ HtmlA.id "built-in-selector" ]
(decks
|> NonEmptyList.toList
|> List.sortWith (sortClosestFirst (Lang.currentLanguage shared) |> Order.map (.language >> Just))
|> List.map deck
)
]
Nothing ->
@@ -107,26 +144,32 @@ editor (Id selectedId) shared _ update =
details : Id -> Shared -> Source.Details
details (Id given) shared =
details given shared =
let
isSame deckInfo =
case deckInfo.id of
Id other ->
given == other
isSame { id } =
given == id
info =
shared.sources.builtIn
|> Maybe.andThen (.decks >> NonEmptyList.toList >> List.filter isSame >> List.head)
in
{ name =
shared.sources.builtIn
|> Maybe.andThen (.decks >> List.filter isSame >> List.head >> Maybe.map .name)
|> Maybe.withDefault ""
{ name = info |> Maybe.map .name |> Maybe.withDefault ""
, url = Nothing
, author = info |> Maybe.map .author
, translator = info |> Maybe.andThen .translator
, language = info |> Maybe.map .language
}
tooltip : Id -> Shared -> Maybe ( String, Html msg )
tooltip (Id given) shared =
( "builtin-" ++ given, Html.span [] [ details (Id given) shared |> .name |> Html.text ] ) |> Just
tooltip : Id -> (String -> List (Html msg) -> Html msg) -> Maybe ( String, Html msg )
tooltip id renderTooltip =
let
forId =
"builtin-" ++ (id |> toString)
in
( forId, renderTooltip forId [] ) |> Just
logo : () -> Maybe (Html msg)
logo () =
Nothing
Icon.massiveDecks |> Icon.viewIcon |> Just
@@ -1,8 +1,21 @@
module MassiveDecks.Card.Source.BuiltIn.Model exposing (Id(..))
module MassiveDecks.Card.Source.BuiltIn.Model exposing
( Deck
, Id
, Info
, fromString
, hardcoded
, idDecoder
, toString
)
{-| Models for the built-in source.
-}
import Json.Decode as Json
import List.Extra as List
import MassiveDecks.Strings.Languages.Model exposing (Language)
import MassiveDecks.Util.NeList as NonEmptyList exposing (NeList)
{-| The id for a built-in deck.
-}
@@ -10,8 +23,56 @@ type Id
= Id String
{-| Create an id from a string.
{-| Information about the source from the server.
-}
playCode : String -> Id
playCode string =
string |> Id
type alias Info =
{ decks : NeList Deck }
{-| Information about a deck.
-}
type alias Deck =
{ name : String
, id : Id
, language : Language
, author : String
, translator : Maybe String
}
{-| Get an id from a string.
-}
fromString : Maybe Info -> String -> Maybe Id
fromString builtInInfo stringId =
let
matching got deck =
case deck.id of
Id str ->
str == got
internal { decks } =
decks |> NonEmptyList.toList |> List.find (matching stringId) |> Maybe.map .id
in
builtInInfo |> Maybe.andThen internal
{-| Allows you to hard-code an Id for a deck. This means that the client and server could end up out of sync if the id
doesn't actually exist.
-}
hardcoded : String -> Id
hardcoded =
Id
{-| A decoder for ids.
-}
idDecoder : Json.Decoder Id
idDecoder =
Json.string |> Json.map Id
{-| Convert an Id to it's string representation.
-}
toString : Id -> String
toString (Id str) =
str
@@ -6,18 +6,18 @@ module MassiveDecks.Card.Source.Cardcast exposing
import Html as Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Json.Decode as Json
import MassiveDecks.Card.Source.Cardcast.Model exposing (..)
import MassiveDecks.Card.Source.Methods as Source
import MassiveDecks.Card.Source.Model as Source exposing (Source)
import MassiveDecks.Components.Form.Message as Message exposing (Message)
import MassiveDecks.Model exposing (..)
import MassiveDecks.Pages.Lobby.Configure.Decks.Model as Decks exposing (DeckOrError)
import MassiveDecks.Pages.Lobby.Configure.Decks.Model exposing (DeckOrError)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Maybe as Maybe
import Material.TextField as TextField
import Url.Builder as Url
import Weightless as Wl
import Weightless.Attributes as WlA
methods : PlayCode -> Source.ExternalMethods msg
@@ -47,9 +47,9 @@ generalMethods =
{- Private -}
id : () -> String
id : () -> Source.General
id () =
"Cardcast"
Source.GCardcast
name : () -> MdString
@@ -58,7 +58,7 @@ name () =
empty : Shared -> Source.External
empty shared =
empty _ =
"" |> playCode |> Source.Cardcast
@@ -81,23 +81,14 @@ problems (PlayCode pc) () =
[]
editor : PlayCode -> Shared -> List DeckOrError -> (Source.External -> msg) -> Html msg
editor (PlayCode pc) shared existing update =
editor : PlayCode -> Shared -> List DeckOrError -> (Source.External -> msg) -> Maybe msg -> msg -> Html msg
editor (PlayCode pc) shared existing update submit noOp =
let
notAlreadyInGame potential =
case potential of
Source.Cardcast playCode ->
let
notSameDeck deckOrError =
let
source =
case deckOrError of
Decks.D d ->
d.source
Decks.E e ->
e.source
in
notSameDeck { source } =
equals playCode source |> not
in
playCode |> Maybe.justIf (existing |> List.all notSameDeck)
@@ -111,14 +102,16 @@ editor (PlayCode pc) shared existing update =
Html.div [ HtmlA.class "primary" ]
[ Html.datalist [ HtmlA.id "cardcast-recent-decks" ]
(shared.settings.settings.recentDecks |> List.filterMap notAlreadyInGame |> List.map recentDeck)
, Wl.textField
[ HtmlA.value pc
, WlA.outlined
, WlA.list "cardcast-recent-decks"
, TextField.view shared
Strings.CardcastPlayCode
TextField.Text
pc
[ HtmlA.list "cardcast-recent-decks"
, HtmlE.onInput (playCode >> Source.Cardcast >> update)
, Strings.CardcastPlayCode |> Lang.label shared
, HtmlE.keyCode
|> Json.map (\k -> submit |> Maybe.andThen (Maybe.justIf (k == 13)) |> Maybe.withDefault noOp)
|> HtmlE.on "keydown"
]
[ Html.span [ WlA.textFieldSlot WlA.BeforeText ] [ logoInternal ] ]
]
@@ -126,12 +119,19 @@ details : PlayCode -> Shared -> Source.Details
details (PlayCode pc) shared =
{ name = (() |> name |> Lang.string shared) ++ " " ++ pc
, url = Just (Url.crossOrigin "https://www.cardcastgame.com" [ "browse", "deck", pc ] [])
, author = Nothing
, translator = Nothing
, language = Nothing
}
tooltip : PlayCode -> Shared -> Maybe ( String, Html msg )
tooltip (PlayCode pc) _ =
( "cardcast-" ++ pc, Html.span [] [ logoInternal, Html.text pc ] ) |> Just
tooltip : PlayCode -> (String -> List (Html msg) -> Html msg) -> Maybe ( String, Html msg )
tooltip (PlayCode pc) tooltipRender =
let
forId =
"cardcast-" ++ pc
in
( forId, [ Html.p [ HtmlA.class "play-code" ] [ logoInternal, Html.text pc ] ] |> tooltipRender forId ) |> Just
logo : () -> Maybe (Html msg)
@@ -14,6 +14,9 @@ methods =
\shared ->
{ name = name shared
, url = Nothing
, translator = Nothing
, author = Nothing
, language = Nothing
}
, tooltip = \_ -> Nothing
}
@@ -13,6 +13,9 @@ methods name =
\_ ->
{ name = name |> Maybe.withDefault ""
, url = Nothing
, author = Nothing
, translator = Nothing
, language = Nothing
}
, tooltip = \_ -> Nothing
}
@@ -74,7 +74,7 @@ type alias ExternalGeneralMethods msg =
-}
type alias IsSpecific general msg =
{ general
| tooltip : Shared -> Maybe ( String, Html msg )
| tooltip : (String -> List (Html msg) -> Html msg) -> Maybe ( String, Html msg )
, defaultDetails : Shared -> Details
}
@@ -83,7 +83,7 @@ type alias IsSpecific general msg =
-}
type alias IsSpecificExternal general msg =
{ general
| editor : Shared -> List DeckOrError -> (External -> msg) -> Html msg
| editor : Shared -> List DeckOrError -> (External -> msg) -> Maybe msg -> msg -> Html msg
, equals : External -> Bool
, problems : () -> List (Message msg)
}
@@ -94,5 +94,5 @@ type alias IsSpecificExternal general msg =
type alias IsGeneralExternal rest =
{ rest
| empty : Shared -> External
, id : () -> String
, id : () -> General
}
@@ -1,16 +1,27 @@
module MassiveDecks.Card.Source.Model exposing
( BuiltInDeck
, BuiltInInfo
, Details
( Details
, External(..)
, General(..)
, Info
, LoadFailureReason(..)
, Source(..)
, Summary
, generalDecoder
, generalFromString
, generalToString
)
import Json.Decode as Json
import MassiveDecks.Card.Source.BuiltIn.Model as BuiltIn
import MassiveDecks.Card.Source.Cardcast.Model as Cardcast
import MassiveDecks.Strings.Languages.Model exposing (Language)
{-| A representation of a source in general terms, not a specific deck.
-}
type General
= GBuiltIn
| GCardcast
{-| Details on where game data came from.
@@ -53,6 +64,9 @@ type alias Summary =
type alias Details =
{ name : String
, url : Maybe String
, author : Maybe String
, language : Maybe Language
, translator : Maybe String
}
@@ -66,14 +80,49 @@ type LoadFailureReason
{-| Information about what sources are available from the server.
-}
type alias Info =
{ builtIn : Maybe BuiltInInfo
{ builtIn : Maybe BuiltIn.Info
, cardcast : Bool
}
type alias BuiltInInfo =
{ decks : List BuiltInDeck }
{-| Get a string name from a general source.
-}
generalToString : General -> String
generalToString source =
case source of
GBuiltIn ->
"BuiltIn"
GCardcast ->
"Cardcast"
type alias BuiltInDeck =
{ name : String, id : BuiltIn.Id }
{-| Get a general source by a string name.
-}
generalFromString : String -> Maybe General
generalFromString sourceName =
case sourceName of
"BuiltIn" ->
Just GBuiltIn
"Cardcast" ->
Just GCardcast
_ ->
Nothing
{-| A Json decoder for general sources.
-}
generalDecoder : Json.Decoder General
generalDecoder =
let
internal name =
case generalFromString name of
Just general ->
Json.succeed general
Nothing ->
Json.fail ("Unknown source '" ++ name ++ "'.")
in
Json.string |> Json.andThen internal
+1 -31
View File
@@ -1,19 +1,11 @@
module MassiveDecks.Components exposing
( floatingActionButton
, iconButton
, iconButtonStyled
, linkButton
)
module MassiveDecks.Components exposing (linkButton)
{-| Reusable interface elements.
-}
import FontAwesome.Icon as Icon exposing (Icon)
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Attributes.Aria as Aria
import Weightless as Wl
import Weightless.Attributes as WlA
{-| Something that looks like a link but is actually a button suitable for handling events on click.
@@ -21,25 +13,3 @@ import Weightless.Attributes as WlA
linkButton : List (Html.Attribute msg) -> List (Html msg) -> Html msg
linkButton attrs contents =
Html.span (HtmlA.class "link-button" :: Aria.role "button" :: HtmlA.tabindex 0 :: attrs) contents
{-| A button that is just an icon.
-}
iconButton : List (Html.Attribute msg) -> Icon -> Html msg
iconButton attrs icon =
iconButtonStyled attrs ( [], icon )
{-| A button that is just an icon with styles on the icon.
-}
iconButtonStyled : List (Html.Attribute msg) -> ( List (Html.Attribute msg), Icon ) -> Html msg
iconButtonStyled attrs ( styles, icon ) =
Wl.button (List.concat [ [ WlA.fab, WlA.inverted, WlA.flat ], attrs ]) [ Icon.viewStyled styles icon ]
{-| A circular button designed to be the primary action on a page.
Only one of these should exist on screen at any time.
-}
floatingActionButton : List (Html.Attribute msg) -> Icon -> Html msg
floatingActionButton attrs icon =
Wl.button (List.concat [ [ WlA.fab ], attrs ]) [ Icon.viewIcon icon ]
@@ -5,6 +5,7 @@ module MassiveDecks.Components.Form.Message exposing
, error
, errorWithFix
, info
, infoWithFix
, mdError
, none
, view
@@ -16,12 +17,12 @@ import FontAwesome.Icon as Icon exposing (Icon)
import FontAwesome.Solid as Icon
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import MassiveDecks.Components as Components
import MassiveDecks.Model exposing (..)
import MassiveDecks.Models.MdError as MdError exposing (MdError)
import MassiveDecks.Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.NeList as NeList exposing (NeList(..))
import Material.IconButton as IconButton
import Svg.Attributes as Svg
@@ -56,6 +57,15 @@ info mdString =
}
infoWithFix : MdString -> List (Fix msg) -> Message msg
infoWithFix mdString fixes =
Just
{ severity = Info
, description = mdString
, fixes = fixes
}
warning : MdString -> Message msg
warning mdString =
Just
@@ -127,10 +137,4 @@ internalMessage shared { severity, description, fixes } =
viewFix : Shared -> Fix msg -> Html msg
viewFix shared { icon, description, action } =
Html.li []
[ Components.iconButton
[ action |> HtmlE.onClick
, description |> Lang.title shared
]
icon
]
Html.li [] [ IconButton.view shared description (icon |> Icon.present |> NeList.just) (Just action) ]
+44 -73
View File
@@ -1,49 +1,46 @@
module MassiveDecks.Components.Menu exposing
( Item
, Menu
, Part(..)
, button
( button
, link
, open
, toggle
, view
)
import FontAwesome.Attributes as Icon
import FontAwesome.Icon as Icon exposing (Icon)
import FontAwesome.Icon exposing (Icon)
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Html.Attributes.Aria as HtmlA
import MassiveDecks.Components.Menu.Model exposing (..)
import MassiveDecks.Model exposing (..)
import MassiveDecks.Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Html as Html
import MassiveDecks.Util.Html.Attributes as HtmlA
import MassiveDecks.Util.Maybe as Maybe
import Weightless as Wl
import Weightless.Attributes as WlA
import Material.ListView as ListView
import Material.Menu as Menu
{-| A menu.
{-| Toggle the state of a menu.
-}
type alias Menu msg =
List (Part msg)
toggle : State -> State
toggle state =
case state of
Open ->
Closed
Closed ->
Open
{-| A part of a menu.
{-| Get an open state from a boolean indicating if the menu is open.
-}
type Part msg
= Button Item (Maybe msg)
| Link Item (Maybe String)
| Separator
| Nothing
open : Bool -> State
open isOpen =
if isOpen then
Open
{-| A menu item
-}
type alias Item =
{ icon : Icon
, text : MdString
, description : MdString
}
else
Closed
{-| Convenience function for a menu button.
@@ -62,28 +59,9 @@ link icon text description href =
{-| Render a menu to Html.
-}
view :
Shared
-> String
-> ( WlA.XOrigin, WlA.YOrigin )
-> ( WlA.XOrigin, WlA.YOrigin )
-> Menu msg
-> Html msg
view shared anchorId ( xAnchor, yAnchor ) ( xTransform, yTransform ) menu =
Wl.popover
(List.concat
[ [ WlA.anchor anchorId
, WlA.fixed
, WlA.anchorOpenEvents [ "click" ]
, HtmlA.id "game-menu"
, HtmlA.class "menu"
, WlA.disableFocusTrap
]
, WlA.anchorOrigin xAnchor yAnchor
, WlA.transformOrigin xTransform yTransform
]
)
[ Wl.popoverCard [] [ Html.ul [] (menu |> List.map (menuItem shared)) ] ]
view : Shared -> msg -> State -> Corner -> Html msg -> Menu msg -> Html msg
view shared onClose state corner anchor menu =
Menu.view onClose state corner anchor (menu |> List.map (menuItem shared))
@@ -93,33 +71,26 @@ view shared anchorId ( xAnchor, yAnchor ) ( xTransform, yTransform ) menu =
menuItem : Shared -> Part msg -> Html msg
menuItem shared mi =
case mi of
Button item action ->
Html.li []
[ listItem shared item (Maybe.isJust action) (action |> Maybe.map HtmlE.onClick |> Maybe.withDefault HtmlA.nothing)
]
Button { icon, text, description } action ->
ListView.viewItem (ListView.action action)
(Just icon)
Nothing
Nothing
[ text |> Lang.string shared |> Html.text ]
Link item href ->
Html.li []
[ Html.blankA [ href |> Maybe.map HtmlA.href |> Maybe.withDefault HtmlA.nothing ]
[ listItem shared item (Maybe.isJust href) HtmlA.nothing ]
Link { icon, text, description } href ->
-- TODO: Deal with being disabled in a decent way.
Html.blankA [ href |> Maybe.map HtmlA.href |> Maybe.withDefault HtmlA.nothing ]
[ ListView.viewItem
ListView.Link
(Just icon)
Nothing
Nothing
[ text |> Lang.string shared |> Html.text ]
]
Separator ->
Html.li [] [ Html.hr [] [] ]
Html.li [ HtmlA.attribute "divider" "divider", HtmlA.role "separator" ] []
Nothing ->
Ignore ->
Html.nothing
listItem : Shared -> Item -> Bool -> Html.Attribute msg -> Html msg
listItem shared { icon, text, description } enabled attr =
Wl.listItem
[ description |> Lang.title shared
, WlA.clickable |> Maybe.justIf enabled |> Maybe.withDefault WlA.disabled
, attr
]
[ Icon.viewStyled [ WlA.listItemSlot WlA.BeforeItem, Icon.fw ] icon
-- We have an icon already, so we don't want to enhance this.
, text |> Lang.string shared |> Html.text
]
@@ -0,0 +1,54 @@
module MassiveDecks.Components.Menu.Model exposing
( Corner(..)
, Item
, Menu
, Part(..)
, State(..)
)
import FontAwesome.Icon exposing (Icon)
import MassiveDecks.Strings exposing (MdString)
{-| A menu.
-}
type alias Menu msg =
List (Part msg)
{-| A part of a menu.
-}
type Part msg
= Button Item (Maybe msg)
| Link Item (Maybe String)
| Separator
| Ignore
{-| A menu item
-}
type alias Item =
{ icon : Icon
, text : MdString
, description : MdString
}
{-| The corner of the anchor element the menu should position itself at.
-}
type Corner
= TopLeft
| TopRight
| BottomLeft
| BottomRight
| TopStart
| TopEnd
| BottomStart
| BottomEnd
{-| If the menu is visible or not.
-}
type State
= Open
| Closed
+21 -26
View File
@@ -16,8 +16,6 @@ import MassiveDecks.Strings.Languages.Model as Lang exposing (Language)
import MassiveDecks.Util.Html as Html
import MassiveDecks.Version exposing (version)
import Url.Builder
import Weightless as Wl
import Weightless.Attributes as WlA
{-| A view of an error.
@@ -28,32 +26,31 @@ view shared route error =
model =
render error
report =
rawReport =
model.details |> Maybe.map (body shared route model.description)
reportView =
report |> Maybe.map (viewReport shared) |> Maybe.withDefault []
( report, link ) =
rawReport |> Maybe.map (viewReport shared) |> Maybe.withDefault ( Html.nothing, Html.nothing )
in
Wl.expansion
[ WlA.name "errors"
, HtmlA.class "error"
, HtmlA.disabled (report == Nothing)
, WlA.noRipple (report == Nothing)
Html.div
[ HtmlA.class "error" ]
[ Html.div [ HtmlA.class "header" ]
[ Html.span [ HtmlA.class "title" ] [ Strings.Error |> Lang.html shared ]
, Html.span [ HtmlA.class "description" ] [ model.description |> Lang.html shared ]
, link
]
, report
]
(Html.span [ HtmlA.class "title", WlA.expansionSlot WlA.ETitle ] [ Strings.Error |> Lang.html shared ]
:: Html.span [ HtmlA.class "description", WlA.expansionSlot WlA.EDescription ] [ model.description |> Lang.html shared ]
:: reportView
)
{-| A view of an MdError.
-}
viewSpecific : Shared -> MdError -> Html msg
viewSpecific shared error =
Wl.expansion
[ WlA.name "errors", HtmlA.class "error", HtmlA.attribute "open" "open" ]
[ Html.span [ HtmlA.class "title", WlA.expansionSlot WlA.ETitle ] [ Strings.Error |> Lang.html shared ]
, Html.span [ WlA.expansionSlot WlA.EDescription ] [ error |> MdError.shortDescribe |> Lang.html shared ]
Html.div
[ HtmlA.class "error" ]
[ Html.span [ HtmlA.class "title" ] [ Strings.Error |> Lang.html shared ]
, Html.span [] [ error |> MdError.shortDescribe |> Lang.html shared ]
, Html.p [] [ error |> MdError.describe |> Lang.html shared ]
]
@@ -62,7 +59,7 @@ viewSpecific shared error =
{- Private -}
viewReport : Shared -> String -> List (Html msg)
viewReport : Shared -> String -> ( Html msg, Html msg )
viewReport shared report =
let
github =
@@ -77,15 +74,13 @@ viewReport shared report =
++ report
url =
Url.Builder.crossOrigin github path [ Url.Builder.string "body" reportBody ]
Url.Builder.crossOrigin github path [ reportBody |> String.left 1950 |> Url.Builder.string "body" ]
in
[ Html.div [ HtmlA.class "report" ]
[ Wl.textArea [ HtmlA.class "body", WlA.readonly, WlA.value report ] []
, Html.span [ HtmlA.class "link" ]
[ Html.blankA [ HtmlA.href url ] [ Strings.ReportError |> Lang.html shared ]
]
( Html.textarea [ HtmlA.class "report", HtmlA.readonly True, HtmlA.value report ] []
, Html.span [ HtmlA.class "link" ]
[ Html.blankA [ HtmlA.href url ] [ Strings.ReportError |> Lang.html shared ]
]
]
)
body : Shared -> Route -> MdString -> String -> String
+13 -16
View File
@@ -1,10 +1,9 @@
module MassiveDecks.Error.Overlay exposing (init, update, view)
import FontAwesome.Icon as Icon
import FontAwesome.Solid as Icon
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import MassiveDecks.Components as Components
import MassiveDecks.Error as Error
import MassiveDecks.Error.Messages exposing (..)
import MassiveDecks.Error.Model as Error exposing (Error)
@@ -13,7 +12,9 @@ import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Pages.Route exposing (Route)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import Weightless as Wl
import MassiveDecks.Util.NeList as NeList exposing (NeList(..))
import Material.Card as Card
import Material.IconButton as IconButton
init : Error.Overlay
@@ -29,20 +30,16 @@ view shared route model =
else
[ Html.div [ HtmlA.class "error-overlay" ]
[ Html.div [ HtmlA.class "actions" ]
[ Components.iconButton
[ HtmlA.class "close"
, Clear |> Global.ErrorMsg |> HtmlE.onClick
, Strings.Close |> Lang.title shared
]
Icon.times
, Components.iconButton
[ HtmlA.class "refresh"
, Global.Refresh |> HtmlE.onClick
, Strings.Refresh |> Lang.title shared
]
Icon.redo
[ IconButton.view shared
Strings.Close
(Icon.times |> Icon.present |> NeList.just)
(Clear |> Global.ErrorMsg |> Just)
, IconButton.view shared
Strings.Refresh
(Icon.redo |> Icon.present |> NeList.just)
(Just Global.Refresh)
]
, Wl.card [ HtmlA.class "help" ]
, Card.view [ HtmlA.class "help" ]
[ Html.h3 [] [ Lang.html shared Strings.ErrorHelpTitle ]
, Html.p [] [ Lang.html shared Strings.ErrorHelp ]
]
+131 -101
View File
@@ -9,7 +9,9 @@ module MassiveDecks.Game exposing
)
import Browser.Dom as Dom
import Browser.Events as Browser
import Dict exposing (Dict)
import FontAwesome.Attributes as Icon
import FontAwesome.Icon as Icon exposing (Icon)
import FontAwesome.Solid as Icon
import Html exposing (Html)
@@ -39,18 +41,23 @@ import MassiveDecks.Notifications as Notifications
import MassiveDecks.Pages.Lobby.Actions as Actions
import MassiveDecks.Pages.Lobby.Configure.Model exposing (Config)
import MassiveDecks.Pages.Lobby.Events as Events exposing (Event)
import MassiveDecks.Pages.Lobby.Messages as Lobby
import MassiveDecks.Pages.Lobby.Model as Lobby exposing (Lobby)
import MassiveDecks.Pages.Lobby.Route as Lobby
import MassiveDecks.Ports as Ports
import MassiveDecks.Speech as Speech
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.User as User exposing (User)
import MassiveDecks.Util.Html as Html
import MassiveDecks.Util.Maybe as Maybe
import MassiveDecks.Util.NeList as NeList
import Material.Button as Button
import Material.Card as Card
import Material.IconButton as IconButton
import Material.LinearProgress as LinearProgress
import Set exposing (Set)
import Task
import Weightless as Wl
import Weightless.Attributes as WlA
import Weightless.ProgressBar as ProgressBar
init : (Msg -> msg) -> Game -> List Card.Response -> Round.Pick -> ( Model, Cmd msg )
@@ -299,6 +306,12 @@ update wrap shared msg model =
EnforceTimeLimit ->
( model, Actions.enforceTimeLimit (model.game.round |> Round.data |> .id) (model.game.round |> Round.stage) )
ToggleHelp ->
( { model | helpVisible = not model.helpVisible }, Cmd.none )
Confetti ->
( { model | confetti = True }, Ports.startConfetti confettiId )
NoOp ->
( model, Cmd.none )
@@ -308,37 +321,47 @@ subscriptions wrap anchor model =
let
( startedAt, maybeLimitSeconds, timedOut ) =
roundTimeDetails model.game
in
if timedOut || model.game.winner /= Nothing then
Sub.none
else
case maybeLimitSeconds of
Nothing ->
timerSub =
if timedOut || model.game.winner /= Nothing then
Sub.none
Just limitSeconds ->
case model.time of
Just time ->
let
left =
timeLeft anchor startedAt time limitSeconds
in
if left < 0 then
Sub.none
else if left > showProgressBarForLast then
Time.every 500 (UpdateTimer >> wrap)
else
Time.animate (UpdateTimer >> wrap)
else
case maybeLimitSeconds of
Nothing ->
Time.animate (UpdateTimer >> wrap)
Sub.none
Just limitSeconds ->
case model.time of
Just time ->
let
left =
timeLeft anchor startedAt time limitSeconds
in
if left < 0 then
Sub.none
else if left > showProgressBarForLast then
Time.every 500 (UpdateTimer >> wrap)
else
Time.animate (UpdateTimer >> wrap)
Nothing ->
Time.animate (UpdateTimer >> wrap)
confettiSub =
if model.game.winner /= Nothing && not model.confetti then
Browser.onAnimationFrame (Confetti |> wrap |> always)
else
Sub.none
in
Sub.batch [ timerSub, confettiSub ]
view : (Msg -> msg) -> Shared -> Lobby.Auth -> Time.Anchor -> String -> Config -> Dict User.Id User -> Model -> Html msg
view wrap shared auth timeAnchor name config users model =
view : (Lobby.Msg -> msg) -> (Msg -> msg) -> Shared -> Lobby.Auth -> Time.Anchor -> String -> Config -> Dict User.Id User -> Model -> Html msg
view wrapLobby wrap shared auth timeAnchor name config users model =
let
overlay =
if model.game.players |> Dict.get auth.claims.uid |> Maybe.map (\p -> p.presence == Player.Away) |> Maybe.withDefault False then
@@ -357,7 +380,7 @@ view wrap shared auth timeAnchor name config users model =
else
case model.game.winner of
Just winners ->
viewWinner shared users winners
viewWinner wrapLobby shared users auth.claims.uid winners
Nothing ->
viewRound wrap shared auth timeAnchor config users model
@@ -687,7 +710,7 @@ applyGameEvent wrap wrapEvent auth shared gameEvent model =
game =
model.game
in
( { model | game = { game | winner = Just winner } }, Cmd.none )
( { model | game = { game | winner = Just winner }, confetti = False }, Cmd.none )
applyGameStarted : (Msg -> msg) -> Lobby -> Round.Playing -> List Card.Response -> ( Model, Cmd msg )
@@ -748,12 +771,34 @@ updateLikes plays uid player =
|> Maybe.withDefault player
viewWinner : Shared -> Dict User.Id User -> Set User.Id -> List (Html msg)
viewWinner shared users winners =
[ Wl.card [ HtmlA.id "game-winner" ]
confettiId : String
confettiId =
"win-confetti-overlay"
viewWinner : (Lobby.Msg -> msg) -> Shared -> Dict User.Id User -> User.Id -> Set User.Id -> List (Html msg)
viewWinner wrapLobby shared users localUser winners =
let
configureNextGame =
if users |> Dict.get localUser |> Maybe.map (.privilege >> (==) User.Privileged) |> Maybe.withDefault False then
Button.view shared
Button.Raised
Strings.ConfigureNextGame
Strings.ConfigureNextGame
(Icon.cog |> Icon.viewIcon)
[ HtmlA.id "new-game-config"
, Lobby.Configure |> Just |> Lobby.ChangeSection |> wrapLobby |> HtmlE.onClick
]
else
Html.nothing
in
[ Card.view [ HtmlA.id "game-winner" ]
[ Html.span [] [ Icon.trophy |> Icon.viewIcon ]
, Html.ul [] (winners |> Set.toList |> List.map (viewWinnerListItem shared users))
]
, configureNextGame
, Html.canvas [ HtmlA.id confettiId ] []
]
@@ -798,16 +843,16 @@ viewRound wrap shared auth timeAnchor config users model =
renderedCall =
call |> Call.viewFilled shared config Card.Front [] parts
{ bar, countdown } =
timer timeAnchor model
in
[ Html.div [ HtmlA.id "top-content" ]
[ bar |> Maybe.withDefault Html.nothing
, Html.div [ HtmlA.class "top-row" ]
[ minorActions wrap shared auth game instruction
, countdown |> Maybe.withDefault Html.nothing
]
[ case instruction |> Maybe.andThen (Maybe.justIf model.helpVisible) of
Just i ->
Card.view [ HtmlA.class "help" ] [ Icon.questionCircle |> Icon.viewIcon, i |> Lang.html shared ]
Nothing ->
Html.nothing
, timer timeAnchor model
, Html.div [ HtmlA.class "top-row" ] [ minorActions wrap shared auth game model.helpVisible ]
]
, Html.div [ HtmlA.class "round" ] [ renderedCall, viewAction wrap shared action ]
, content
@@ -815,41 +860,27 @@ viewRound wrap shared auth timeAnchor config users model =
-- TODO: Hide this when at top. Waiting on native elm scroll events, as currently we'd have to ping constantly.
, Html.div [ HtmlA.class "scroll-top" ]
[ Wl.button
[ WlA.flat
, WlA.fab
, WlA.inverted
, ScrollToTop |> wrap |> HtmlE.onClick
]
[ Icon.viewIcon Icon.arrowUp ]
]
[ IconButton.view shared Strings.ScrollToTop (Icon.arrowUp |> Icon.present |> NeList.just) (ScrollToTop |> wrap |> Just) ]
]
help : Shared -> MdString -> Html msg
help shared instruction =
toggleHelp : (Msg -> msg) -> Shared -> Bool -> Html msg
toggleHelp wrap shared enabled =
let
id =
"context-help-button"
extra =
if enabled then
[ Icon.slash |> Icon.present |> Icon.styled [ Icon.fw ] ]
helpContent =
[ Components.iconButton
[ HtmlA.id id, Strings.ViewHelpAction |> Lang.title shared ]
Icon.question
, Wl.popover
(List.concat
[ WlA.anchorOrigin WlA.XCenter WlA.YBottom
, WlA.transformOrigin WlA.XLeft WlA.YTop
, [ WlA.anchor id, WlA.fixed, WlA.anchorOpenEvents [ "click" ] ]
]
)
[ Wl.popoverCard [] [ instruction |> Lang.html shared ] ]
]
else
[]
icon =
Icon.question |> Icon.present |> Icon.styled [ Icon.fw ] |> NeList.just |> NeList.extend extra
in
Html.div [ HtmlA.id "context-help" ] helpContent
IconButton.view shared Strings.ViewHelpAction icon (ToggleHelp |> wrap |> Just)
timer : Time.Anchor -> Model -> { bar : Maybe (Html msg), countdown : Maybe (Html msg) }
timer : Time.Anchor -> Model -> Html msg
timer timeAnchor model =
let
( startedAt, limit, timedOut ) =
@@ -862,10 +893,10 @@ timer timeAnchor model =
else
Maybe.map2 (timeLeft timeAnchor startedAt) model.time limit
in
left |> Maybe.map timerInternal |> Maybe.withDefault { bar = Nothing, countdown = Nothing }
left |> Maybe.andThen timerInternal |> Maybe.withDefault (Html.div [ HtmlA.id "timer" ] [])
timerInternal : Int -> { bar : Maybe (Html msg), countdown : Maybe (Html msg) }
timerInternal : Int -> Maybe (Html msg)
timerInternal leftInt =
let
last =
@@ -873,20 +904,18 @@ timerInternal leftInt =
left =
leftInt |> toFloat
progressBar =
(1 - (last - left) / last)
|> timerProgressBar
in
{ bar = progressBar
, countdown = Just (Html.div [ HtmlA.id "time-left" ] [ max 0 left / 1000 |> ceiling |> String.fromInt |> Html.text ])
}
(1 - (last - left) / last) |> timerProgressBar
timerProgressBar : Float -> Maybe (Html msg)
timerProgressBar proportion =
if proportion < 1 then
ProgressBar.determinate (min 1 (max 0 proportion)) [ HtmlA.id "timer" ] |> Just
let
progress =
proportion |> max 0 |> LinearProgress.Progress
in
LinearProgress.view progress [ HtmlA.id "timer" ] |> Just
else
Nothing
@@ -894,7 +923,7 @@ timerProgressBar proportion =
showProgressBarForLast : Int
showProgressBarForLast =
20000
15000
timeLeft : Time.Anchor -> Time -> Time -> Int -> Int
@@ -929,8 +958,8 @@ roundTimeDetails game =
( complete.startedAt, Nothing, False )
minorActions : (Msg -> msg) -> Shared -> Lobby.Auth -> Game -> Maybe MdString -> Html msg
minorActions wrap shared auth game instruction =
minorActions : (Msg -> msg) -> Shared -> Lobby.Auth -> Game -> Bool -> Html msg
minorActions wrap shared auth game helpEnabled =
let
localPlayer =
game.players |> Dict.get auth.claims.uid
@@ -940,11 +969,10 @@ minorActions wrap shared auth game instruction =
enforceTimeLimit =
if timedOut && game.rules.timeLimits.mode == Rules.Soft then
Components.iconButton
[ Strings.EnforceTimeLimitAction |> Lang.title shared
, EnforceTimeLimit |> wrap |> HtmlE.onClick
]
Icon.forward
IconButton.view shared
Strings.EnforceTimeLimitAction
(Icon.forward |> Icon.present |> NeList.just)
(EnforceTimeLimit |> wrap |> Just)
|> Just
else
@@ -953,7 +981,7 @@ minorActions wrap shared auth game instruction =
Html.div [ HtmlA.id "minor-actions" ]
(List.filterMap identity
[ Just (historyButton wrap shared)
, instruction |> Maybe.map (help shared)
, Just (toggleHelp wrap shared helpEnabled)
, Maybe.map2 (\score -> \reboot -> rebootButton wrap shared score reboot)
(localPlayer |> Maybe.map .score)
game.rules.houseRules.reboot
@@ -964,24 +992,26 @@ minorActions wrap shared auth game instruction =
historyButton : (Msg -> msg) -> Shared -> Html msg
historyButton wrap shared =
Components.iconButton
[ HtmlA.id "history-button"
, Strings.ViewGameHistoryAction |> Lang.title shared
, ToggleHistoryView |> wrap |> HtmlE.onClick
]
Icon.history
IconButton.view shared
Strings.ViewGameHistoryAction
(Icon.history |> Icon.present |> NeList.just)
(ToggleHistoryView |> wrap |> Just)
rebootButton : (Msg -> msg) -> Shared -> Int -> Rules.Reboot -> Html msg
rebootButton wrap shared score reboot =
Components.iconButton
[ HtmlA.id "redraw"
, { cost = reboot.cost } |> Strings.HouseRuleRebootAction |> Lang.title shared
, WlA.disabled
|> Maybe.justIf (score < reboot.cost)
|> Maybe.withDefault (Redraw |> wrap |> HtmlE.onClick)
]
Icon.random
let
action =
if score < reboot.cost then
Nothing
else
Redraw |> wrap |> Just
in
IconButton.view shared
({ cost = reboot.cost } |> Strings.HouseRuleRebootAction)
(Icon.random |> Icon.present |> NeList.just)
action
viewAction : (Msg -> msg) -> Shared -> Maybe Action -> Html msg
+10 -17
View File
@@ -1,17 +1,14 @@
module MassiveDecks.Game.Action exposing (actions, view)
import FontAwesome.Icon exposing (Icon)
import FontAwesome.Icon as Icon exposing (Icon)
import FontAwesome.Solid as Icon
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import MassiveDecks.Components as Components
import MassiveDecks.Game.Action.Model exposing (..)
import MassiveDecks.Game.Messages as Game exposing (Msg)
import MassiveDecks.Model exposing (..)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import Weightless.Attributes as WlA
import Material.Fab as Fab
actions : List Action
@@ -23,14 +20,14 @@ actions =
-}
blocking : List (Html.Attribute msg)
blocking =
[ HtmlA.class "important" ]
[ HtmlA.class "blocking" ]
{-| Style for an action that doesn't block the game.
-}
normal : List (Html.Attribute msg)
normal =
[ WlA.inverted, WlA.outlined ]
[ HtmlA.class "normal" ]
view : (Msg -> msg) -> Shared -> Maybe Action -> Action -> Html msg
@@ -53,16 +50,12 @@ view wrap shared visible action =
Advance ->
IconView Icon.forward blocking Strings.AdvanceRound Game.AdvanceRound
in
Components.floatingActionButton
(List.concat
[ [ title |> Lang.title shared
, onClick |> wrap |> HtmlE.onClick
, HtmlA.classList [ ( "action", True ), ( "exited", visible /= Just action ) ]
]
, attrs
]
)
icon
Fab.view shared
Fab.Normal
title
(icon |> Icon.present)
(onClick |> wrap |> Just)
(HtmlA.classList [ ( "action", True ), ( "exited", visible /= Just action ) ] :: attrs)
type alias IconView msg =
+7 -9
View File
@@ -1,37 +1,35 @@
module MassiveDecks.Game.History exposing (view)
import Dict exposing (Dict)
import FontAwesome.Icon as Icon
import FontAwesome.Solid as Icon
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Html.Keyed as HtmlK
import MassiveDecks.Card.Call as Call
import MassiveDecks.Card.Model as Card
import MassiveDecks.Card.Play as Play
import MassiveDecks.Card.Response as Response
import MassiveDecks.Components as Components
import MassiveDecks.Game.Messages exposing (Msg(..))
import MassiveDecks.Game.Round as Round
import MassiveDecks.Game.Round.Plays as Plays
import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Pages.Lobby.Configure.Model exposing (Config)
import MassiveDecks.Strings as Strings
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.User as User exposing (User)
import MassiveDecks.Util.Maybe as Maybe
import MassiveDecks.Util.NeList as NeList
import Material.IconButton as IconButton
view : (Msg -> msg) -> Shared -> Config -> Dict User.Id User -> String -> List Round.Complete -> List (Html msg)
view wrap shared config users name history =
[ Html.div [ HtmlA.id "top-content" ]
[ Html.div [ HtmlA.id "minor-actions" ]
[ Components.iconButton
[ HtmlA.id "return-to-game-button"
, Strings.ViewGameHistoryAction |> Lang.title shared
, ToggleHistoryView |> wrap |> HtmlE.onClick
]
Icon.arrowLeft
[ IconButton.view shared
Strings.ViewGameHistoryAction
(Icon.arrowLeft |> Icon.present |> NeList.just)
(ToggleHistoryView |> wrap |> Just)
]
]
, Html.div [ HtmlA.id "history" ]
@@ -26,5 +26,7 @@ type Msg
| SetPresence Player.Presence
| SetPlayerAway User.Id
| UpdateTimer Time
| ToggleHelp
| EnforceTimeLimit
| Confetti
| NoOp
@@ -31,6 +31,8 @@ type alias Model =
, completeRound : Maybe Round.Complete
, viewingHistory : Bool
, time : Maybe Time
, helpVisible : Bool
, confetti : Bool
}
@@ -45,6 +47,8 @@ emptyModel game =
, completeRound = Nothing
, viewingHistory = False
, time = Nothing
, helpVisible = False
, confetti = False
}
@@ -8,11 +8,11 @@ import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Html.Keyed as HtmlK
import List.Extra as List
import MassiveDecks.Card as Card
import MassiveDecks.Card.Call as Call
import MassiveDecks.Card.Model as Card
import MassiveDecks.Card.Response as Response
import MassiveDecks.Card.Source.Model as Source
import MassiveDecks.Game.Action.Model as Action
import MassiveDecks.Game.Messages as Game exposing (Msg)
import MassiveDecks.Game.Model exposing (..)
@@ -23,7 +23,6 @@ import MassiveDecks.Pages.Lobby.Configure.Model exposing (Config)
import MassiveDecks.Pages.Lobby.Model as Lobby
import MassiveDecks.Strings as Strings
import MassiveDecks.User as User exposing (User)
import MassiveDecks.Util.List as List
import MassiveDecks.Util.Random as Random
import Random
import Set exposing (Set)
@@ -125,7 +124,6 @@ viewHandCard wrap shared config filled picked response =
viewBackgroundPlay : Shared -> PlayStyles -> Int -> Set User.Id -> User.Id -> Html msg
viewBackgroundPlay shared playStyles slots played for =
let
-- TODO: Move to css variable --rotation when possible.
cards =
playStyles
|> Dict.get for
@@ -140,7 +138,7 @@ viewBackgroundPlay shared playStyles slots played for =
viewBackgroundPlayCard : Shared -> CardStyle -> Html msg
viewBackgroundPlayCard shared playStyle =
Response.viewUnknown shared
[ "rotate(" ++ String.fromFloat playStyle.rotation ++ "turn)" |> HtmlA.style "transform"
[ "--rotation: " ++ String.fromFloat playStyle.rotation ++ "turn" |> HtmlA.attribute "style"
, HtmlA.class "ignore-minimal-card-size"
]
+36 -23
View File
@@ -54,27 +54,40 @@ import MassiveDecks.Speech as Speech
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Strings.Languages.Model as Lang exposing (Language)
import MassiveDecks.User as User exposing (User)
import MassiveDecks.Util.NeList as NonEmptyList
import Set exposing (Set)
sourceInfo : Json.Decoder Source.Info
sourceInfo =
let
atLeastOne info =
if [ info.builtIn /= Nothing, info.cardcast ] |> List.any identity then
info |> Json.succeed
else
Json.fail "Must have at least one source enabled."
in
Json.succeed Source.Info
|> Json.optional "builtIn" (builtInInfo |> Json.map Just) Nothing
|> Json.optional "cardcast" Json.bool False
|> Json.andThen atLeastOne
builtInInfo : Json.Decoder Source.BuiltInInfo
builtInInfo : Json.Decoder BuiltIn.Info
builtInInfo =
Json.succeed Source.BuiltInInfo
|> Json.required "decks" (Json.list builtInDeck)
Json.succeed BuiltIn.Info
|> Json.required "decks" (NonEmptyList.decoder builtInDeck)
builtInDeck : Json.Decoder Source.BuiltInDeck
builtInDeck : Json.Decoder BuiltIn.Deck
builtInDeck =
Json.succeed Source.BuiltInDeck
Json.succeed BuiltIn.Deck
|> Json.required "name" Json.string
|> Json.required "id" (Json.string |> Json.map BuiltIn.Id)
|> Json.required "id" BuiltIn.idDecoder
|> Json.required "language" language
|> Json.required "author" Json.string
|> Json.optional "translator" (Json.string |> Json.map Just) Nothing
unknownValue : String -> String -> Json.Decoder a
@@ -194,30 +207,27 @@ sourceByName name =
Json.succeed Source.Custom
_ ->
externalSourceByName name |> Json.map Source.Ex
Json.field "source" Source.generalDecoder |> Json.andThen externalSourceByGeneral |> Json.map Source.Ex
externalSource : Json.Decoder Source.External
externalSource =
Json.field "source" Json.string |> Json.andThen externalSourceByName
Json.field "source" Source.generalDecoder |> Json.andThen externalSourceByGeneral
externalSourceByName : String -> Json.Decoder Source.External
externalSourceByName name =
case name of
"BuiltIn" ->
Json.field "id" Json.string |> Json.map (BuiltIn.Id >> Source.BuiltIn)
externalSourceByGeneral : Source.General -> Json.Decoder Source.External
externalSourceByGeneral general =
case general of
Source.GBuiltIn ->
Json.field "id" BuiltIn.idDecoder |> Json.map Source.BuiltIn
"Cardcast" ->
Source.GCardcast ->
Json.field "playCode" Json.string |> Json.map (Cardcast.playCode >> Source.Cardcast)
_ ->
unknownValue "source" name
tokenValidity : Json.Decoder (Dict Lobby.Token Bool)
tokenValidity : Json.Decoder (List Lobby.Token)
tokenValidity =
Json.dict Json.bool
Json.list lobbyToken
lobbyToken : Json.Decoder Lobby.Token
@@ -280,7 +290,7 @@ privacyConfig =
deckOrError : Json.Decoder DeckConfig.DeckOrError
deckOrError =
Json.oneOf [ Json.map DeckConfig.E deckError, Json.map DeckConfig.D deck ]
Json.oneOf [ deckError |> Json.map DeckConfig.errorToDeckOrError, deck |> Json.map DeckConfig.deckToDeckOrError ]
deck : Json.Decoder DeckConfig.Deck
@@ -307,9 +317,12 @@ summary =
details : Json.Decoder Source.Details
details =
Json.map2 Source.Details
(Json.field "name" Json.string)
(Json.maybe (Json.field "url" Json.string))
Json.succeed Source.Details
|> Json.required "name" Json.string
|> Json.optional "url" (Json.string |> Json.map Just) Nothing
|> Json.optional "author" (Json.string |> Json.map Just) Nothing
|> Json.optional "language" (language |> Json.map Just) Nothing
|> Json.optional "translator" (Json.string |> Json.map Just) Nothing
rules : Json.Decoder Rules
@@ -135,13 +135,13 @@ decks d =
deckOrError : Decks.DeckOrError -> Json.Value
deckOrError de =
case de of
Decks.D d ->
deck d
deckOrError d =
case d.result of
Ok s ->
deck { source = d.source, summary = s }
Decks.E e ->
deckError e
Err r ->
deckError { source = d.source, reason = r }
deck : Decks.Deck -> Json.Value
@@ -327,8 +327,8 @@ source s =
Source.Cardcast (Cardcast.PlayCode playCode) ->
Json.object [ ( "source", "Cardcast" |> Json.string ), ( "playCode", playCode |> Json.string ) ]
Source.BuiltIn (BuiltIn.Id id) ->
Json.object [ ( "source", "BuiltIn" |> Json.string ), ( "id", id |> Json.string ) ]
Source.BuiltIn id ->
Json.object [ ( "source", "BuiltIn" |> Json.string ), ( "id", id |> BuiltIn.toString |> Json.string ) ]
language : Language -> Json.Value
+162 -102
View File
@@ -24,8 +24,9 @@ import MassiveDecks.Animated as Animated exposing (Animated)
import MassiveDecks.Card.Model as Card
import MassiveDecks.Cast.Client as Cast
import MassiveDecks.Cast.Model as Cast
import MassiveDecks.Components as Components
import MassiveDecks.Components.Form.Message as Message exposing (Message)
import MassiveDecks.Components.Menu as Menu
import MassiveDecks.Components.Menu.Model as Menu
import MassiveDecks.Error as MdError
import MassiveDecks.Error.Model as Error exposing (Error)
import MassiveDecks.Game as Game
@@ -59,8 +60,10 @@ import MassiveDecks.User as User exposing (User)
import MassiveDecks.Util.Html as Html
import MassiveDecks.Util.Html.Attributes as HtmlA
import MassiveDecks.Util.Maybe as Maybe
import Weightless as Wl
import Weightless.Attributes as WlA
import MassiveDecks.Util.NeList as NeList
import Material.Button as Button
import Material.Card as Card
import Material.IconButton as IconButton
changeRoute : Shared -> Route -> Model -> Route.Fork ( Model, Cmd msg )
@@ -97,6 +100,8 @@ initWithAuth shared r auth =
, inviteDialogOpen = False
, timeAnchor = Nothing
, spectate = Spectate.init
, gameMenu = Menu.Closed
, userMenu = Nothing
}
, ServerConnection.connect auth.claims.gc auth.token
)
@@ -157,20 +162,31 @@ update wrap shared msg model =
applySync wrap shared model state hand play partialTimeAnchor
Events.Configured { change } ->
( applyConfigured change model
, shared
, Cmd.none
)
( applyConfigured change model, shared, Cmd.none )
Events.GameStarted { round, hand } ->
let
( g, cmd ) =
( g, gameCmd ) =
Game.applyGameStarted (GameMsg >> wrap) lobby round (hand |> Maybe.withDefault [])
r =
model.route
changeCmd =
if r.section == Just Configure then
{ r | section = Nothing }
|> Route.Lobby
|> Route.url
|> Navigation.replaceUrl shared.key
|> Just
else
Nothing
cmd =
[ Just gameCmd, changeCmd ] |> List.filterMap identity |> Cmd.batch
in
( Stay { model | lobby = Just { lobby | game = Just g } }
, shared
, cmd
)
( Stay { model | lobby = Just { lobby | game = Just g } }, shared, cmd )
Events.Game gameEvent ->
let
@@ -294,8 +310,11 @@ update wrap shared msg model =
ErrorReceived error ->
case error of
MdError.Authentication reason ->
( AuthError model.auth.claims.gc reason, shared, Cmd.none )
MdError.Authentication _ ->
( JoinError model.auth.claims.gc error, shared, Cmd.none )
MdError.LobbyNotFound _ ->
( JoinError model.auth.claims.gc error, shared, Cmd.none )
MdError.ActionExecution (MdError.ConfigEditConflict _) ->
let
@@ -374,6 +393,28 @@ update wrap shared msg model =
in
( Stay { model | route = newRoute }, shared, newRoute |> Route.Lobby |> Route.url |> Navigation.replaceUrl shared.key )
SetGameMenuState newState ->
( Stay { model | gameMenu = newState }, shared, Cmd.none )
SetUserMenuState id state ->
let
old =
model.userMenu
new =
case state of
Menu.Open ->
Just id
Menu.Closed ->
if old /= Just id then
old
else
Nothing
in
( Stay { model | userMenu = new }, shared, Cmd.none )
NoOp ->
( Stay model, shared, Cmd.none )
@@ -389,7 +430,7 @@ view wrap wrapSettings changePage shared model =
let
viewContent _ auth timeAnchor lobby =
lobby.game
|> Maybe.map (Game.view (GameMsg >> wrap) shared auth timeAnchor lobby.config.name lobby.config lobby.users)
|> Maybe.map (Game.view wrap (GameMsg >> wrap) shared auth timeAnchor lobby.config.name lobby.config lobby.users)
|> Maybe.withDefault (Html.div [] [ Strings.GameNotStartedError |> Lang.html shared ])
in
viewWithUsers wrap wrapSettings shared s viewContent model
@@ -434,7 +475,7 @@ section r auth lobby =
type alias ViewContent msg =
Bool -> Auth -> Time.Anchor -> Lobby -> Html msg
Message msg -> Auth -> Time.Anchor -> Lobby -> Html msg
viewWithUsers : (Msg -> msg) -> (Settings.Msg -> msg) -> Shared -> Section -> ViewContent msg -> Model -> List (Html msg)
@@ -459,7 +500,7 @@ viewWithUsers wrap wrapSettings shared s viewContent model =
castButton =
castAttrs
|> Maybe.map (viewCastButton wrap model.auth)
|> Maybe.map (viewCastButton wrap shared model.auth)
|> Maybe.withDefault []
usersIcon =
@@ -489,12 +530,11 @@ viewWithUsers wrap wrapSettings shared s viewContent model =
(Html.div [ HtmlA.id "top-bar" ]
[ Html.div [ HtmlA.class "left" ]
(List.concat
[ [ Components.iconButtonStyled
[ HtmlE.onClick (usersShown |> not |> Settings.ChangeOpenUserList |> wrapSettings)
, Strings.ToggleUserList |> Lang.title shared
]
( [ Icon.lg ], usersIcon )
, lobbyMenu wrap shared model.route s localUser localPlayer (maybeGame |> Maybe.map .game)
[ [ IconButton.view shared
Strings.ToggleUserList
(usersIcon |> Icon.present |> Icon.styled [ Icon.lg ] |> NeList.just)
(usersShown |> not |> Settings.ChangeOpenUserList |> wrapSettings |> Just)
, lobbyMenu wrap shared model.gameMenu model.route s localUser localPlayer (maybeGame |> Maybe.map .game)
]
, castButton
]
@@ -503,7 +543,7 @@ viewWithUsers wrap wrapSettings shared s viewContent model =
]
:: HtmlK.ol [ HtmlA.class "notifications" ] notifications
:: (model.lobby
|> Maybe.map2 (viewLobby wrap shared model.auth viewContent) model.timeAnchor
|> Maybe.map2 (viewLobby wrap shared model.auth model.userMenu viewContent) model.timeAnchor
|> Maybe.withDefault
[ Html.div [ HtmlA.class "loading" ]
[ Icon.viewStyled [ Icon.spin, Icon.fa3x ] Icon.circleNotch ]
@@ -519,8 +559,8 @@ viewWithUsers wrap wrapSettings shared s viewContent model =
]
viewLobby : (Msg -> msg) -> Shared -> Auth -> ViewContent msg -> Time.Anchor -> Lobby -> List (Html msg)
viewLobby wrap shared auth viewContent timeAnchor lobby =
viewLobby : (Msg -> msg) -> Shared -> Auth -> Maybe User.Id -> ViewContent msg -> Time.Anchor -> Lobby -> List (Html msg)
viewLobby wrap shared auth openUserMenu viewContent timeAnchor lobby =
let
game =
lobby.game |> Maybe.map .game
@@ -528,12 +568,24 @@ viewLobby wrap shared auth viewContent timeAnchor lobby =
privileged =
(lobby.users |> Dict.get auth.claims.uid |> Maybe.map .privilege) == Just User.Privileged
canEdit =
privileged && (game |> Maybe.map (.winner >> Maybe.isJust) |> Maybe.withDefault True)
configDisabledReason =
if not privileged then
Message.info Strings.ConfigurationDisabledIfNotPrivileged
else if game |> Maybe.map (.winner >> Maybe.isNothing) |> Maybe.withDefault False then
Message.infoWithFix Strings.ConfigurationDisabledWhileInGame
[ { description = Strings.EndGame
, icon = Icon.stopCircle
, action = wrap EndGame
}
]
else
Message.none
in
[ Html.div [ HtmlA.id "lobby-content" ]
[ viewUsers wrap shared auth.claims.uid lobby.users game
, Html.div [ HtmlA.id "scroll-frame" ] [ viewContent canEdit auth timeAnchor lobby ]
[ viewUsers wrap shared auth.claims.uid lobby.users openUserMenu game
, Html.div [ HtmlA.id "scroll-frame" ] [ viewContent configDisabledReason auth timeAnchor lobby ]
, lobby.errors |> viewErrors shared
]
]
@@ -552,12 +604,9 @@ cardSizeToAttr cardSize =
HtmlA.nothing
lobbyMenu : (Msg -> msg) -> Shared -> Route -> Section -> Maybe User -> Maybe Player -> Maybe Game -> Html msg
lobbyMenu wrap shared r s user player game =
lobbyMenu : (Msg -> msg) -> Shared -> Menu.State -> Route -> Section -> Maybe User -> Maybe Player -> Maybe Game -> Html msg
lobbyMenu wrap shared menuState r s user player game =
let
id =
"lobby-menu-button"
lobbyMenuItems =
[ Menu.button Icon.bullhorn Strings.InvitePlayers Strings.InvitePlayersDescription (ToggleInviteDialog |> wrap |> Just) ]
@@ -609,11 +658,16 @@ lobbyMenu wrap shared r s user player game =
separatedMenuItems =
menuItems |> List.filterMap identity |> List.intersperse [ Menu.Separator ] |> List.concat
in
Html.div []
[ Components.iconButtonStyled [ HtmlA.id id, Strings.GameMenu |> Lang.title shared ]
( [ Icon.lg ], Icon.bars )
, Menu.view shared id ( WlA.XCenter, WlA.YBottom ) ( WlA.XLeft, WlA.YTop ) separatedMenuItems
]
Menu.view shared
(Menu.Closed |> SetGameMenuState |> wrap)
menuState
Menu.BottomEnd
(IconButton.view shared
Strings.GameMenu
(Icon.bars |> Icon.present |> Icon.styled [ Icon.lg ] |> NeList.just)
(menuState |> Menu.toggle |> SetGameMenuState |> wrap |> Just)
)
separatedMenuItems
applyConfigured : Json.Patch -> Model -> Change
@@ -749,18 +803,19 @@ viewNotification wrap shared users animationState notification =
, Just "error"
)
in
Wl.card
Card.view
[ HtmlA.class "notification", animationState, class |> Maybe.map HtmlA.class |> Maybe.withDefault HtmlA.nothing ]
[ Html.div [ HtmlA.class "content" ]
[ Html.span [ HtmlA.class "icon" ] [ icon ]
, Html.span [ HtmlA.class "message" ] [ message ]
, Wl.button
[ WlA.flat
, WlA.inverted
, notification |> Animated.Exit |> NotificationMsg |> wrap |> HtmlE.onClick
, Button.view shared
Button.Standard
Strings.Dismiss
Strings.Dismiss
Html.nothing
[ notification |> Animated.Exit |> NotificationMsg |> wrap |> HtmlE.onClick
, HtmlA.class "action"
]
[ Strings.Dismiss |> Lang.html shared ]
]
]
@@ -787,8 +842,8 @@ viewError shared error =
error |> MdError.Game |> MdError.viewSpecific shared
viewUsers : (Msg -> msg) -> Shared -> User.Id -> Dict User.Id User -> Maybe Game -> Html msg
viewUsers wrap shared localUserId users game =
viewUsers : (Msg -> msg) -> Shared -> User.Id -> Dict User.Id User -> Maybe User.Id -> Maybe Game -> Html msg
viewUsers wrap shared localUserId users openUserMenu game =
let
localUserPrivilege =
users |> Dict.get localUserId |> Maybe.map .privilege |> Maybe.withDefault User.Unprivileged
@@ -797,24 +852,23 @@ viewUsers wrap shared localUserId users game =
users |> Dict.toList |> List.partition (\( _, user ) -> user.presence == User.Joined)
activeGroups =
active |> byRole |> List.map (viewRoleGroup wrap shared localUserId localUserPrivilege game)
active |> byRole |> List.map (viewRoleGroup wrap shared localUserId localUserPrivilege openUserMenu game)
inactiveGroup =
if List.isEmpty inactive then
[]
else
[ viewUserListGroup wrap shared localUserId localUserPrivilege game ( ( "left", Strings.Left ), inactive ) ]
[ viewUserListGroup wrap shared localUserId localUserPrivilege openUserMenu game ( ( "left", Strings.Left ), inactive ) ]
groups =
List.concat [ activeGroups, inactiveGroup ]
in
Wl.card [ HtmlA.id "users" ]
[ Html.div [ HtmlA.class "collapsible" ] [ HtmlK.ol [] groups ] ]
Card.view [ HtmlA.id "users" ] [ Html.div [ HtmlA.class "collapsible" ] [ HtmlK.ol [] groups ] ]
viewRoleGroup : (Msg -> msg) -> Shared -> User.Id -> User.Privilege -> Maybe Game -> ( User.Role, List ( User.Id, User ) ) -> ( String, Html msg )
viewRoleGroup wrap shared localUserId localUserPrivilege game ( role, users ) =
viewRoleGroup : (Msg -> msg) -> Shared -> User.Id -> User.Privilege -> Maybe User.Id -> Maybe Game -> ( User.Role, List ( User.Id, User ) ) -> ( String, Html msg )
viewRoleGroup wrap shared localUserId localUserPrivilege openUserMenu game ( role, users ) =
let
idAndDescription =
case role of
@@ -824,15 +878,15 @@ viewRoleGroup wrap shared localUserId localUserPrivilege game ( role, users ) =
User.Spectator ->
( "spectators", Strings.Spectators )
in
viewUserListGroup wrap shared localUserId localUserPrivilege game ( idAndDescription, users )
viewUserListGroup wrap shared localUserId localUserPrivilege openUserMenu game ( idAndDescription, users )
viewUserListGroup : (Msg -> msg) -> Shared -> User.Id -> User.Privilege -> Maybe Game -> ( ( String, MdString ), List ( User.Id, User ) ) -> ( String, Html msg )
viewUserListGroup wrap shared localUserId localUserPrivilege game ( ( id, description ), users ) =
viewUserListGroup : (Msg -> msg) -> Shared -> User.Id -> User.Privilege -> Maybe User.Id -> Maybe Game -> ( ( String, MdString ), List ( User.Id, User ) ) -> ( String, Html msg )
viewUserListGroup wrap shared localUserId localUserPrivilege openUserMenu game ( ( id, description ), users ) =
( id
, Html.li [ HtmlA.class id ]
[ Html.span [] [ description |> Lang.html shared ]
, HtmlK.ol [] (users |> List.map (viewUser wrap shared localUserId localUserPrivilege game))
, HtmlK.ol [] (users |> List.map (viewUser wrap shared localUserId localUserPrivilege openUserMenu game))
]
)
@@ -849,15 +903,12 @@ byRole users =
|> List.filter (\( _, us ) -> not (List.isEmpty us))
viewUser : (Msg -> msg) -> Shared -> User.Id -> User.Privilege -> Maybe Game -> ( User.Id, User ) -> ( String, Html msg )
viewUser wrap shared localUserId localUserPrivilege game ( userId, user ) =
viewUser : (Msg -> msg) -> Shared -> User.Id -> User.Privilege -> Maybe User.Id -> Maybe Game -> ( User.Id, User ) -> ( String, Html msg )
viewUser wrap shared localUserId localUserPrivilege openUserMenu game ( userId, user ) =
let
( secondary, score ) =
userDetails shared game userId user
id =
"user-" ++ userId
player =
game |> Maybe.map .players |> Maybe.andThen (Dict.get userId)
@@ -914,10 +965,10 @@ viewUser wrap shared localUserId localUserPrivilege game ( userId, user ) =
in
let
setAway =
if userId == localUserId then
if userId == localUserId && game /= Nothing then
setPresenceMenuItem wrap player |> Just
else if not isAway then
else if not isAway && game /= Nothing then
case localUserPrivilege of
User.Privileged ->
Menu.button Icon.userClock Strings.SetAway Strings.SetAway (userId |> SetAway |> wrap |> Just) |> Just
@@ -946,27 +997,38 @@ viewUser wrap shared localUserId localUserPrivilege game ( userId, user ) =
else
[]
( menu, clickable ) =
( menu, attrs ) =
if user.presence == User.Left || List.isEmpty menuItems then
( Html.nothing, HtmlA.nothing )
( identity, [] )
else
( menuItems |> Menu.view shared id ( WlA.XCenter, WlA.YBottom ) ( WlA.XCenter, WlA.YTop )
, WlA.clickable
let
isOpen =
Just userId == openUserMenu
in
( \c ->
Menu.view shared
(SetUserMenuState userId Menu.Closed |> wrap)
(isOpen |> Menu.open)
Menu.BottomRight
c
menuItems
, [ not isOpen |> Menu.open |> SetUserMenuState userId |> wrap |> HtmlE.onClick
, HtmlA.classList [ ( "active", isOpen ), ( "has-menu", True ) ]
]
)
in
( userId
, Html.li []
[ Wl.listItem
[ HtmlA.classList [ ( "you", localUserId == userId ), ( "away", isAway ) ]
, clickable
, HtmlA.id id
[ Html.div
(HtmlA.classList [ ( "user", True ), ( "you", localUserId == userId ), ( "away", isAway ) ] :: attrs)
[ Html.div [ HtmlA.class "about" ]
[ Html.div [ HtmlA.class "name", HtmlA.title user.name ] [ Html.text user.name ]
, Html.div [ HtmlA.class "state", HtmlA.class "compressed-terms" ] secondary
]
, Html.span [ HtmlA.class "scores" ] score
]
[ Html.span [ HtmlA.class "user", HtmlA.title user.name ] [ Html.text user.name ]
, Html.div [ HtmlA.class "compressed-terms" ] secondary
, Html.span [ WlA.listItemSlot WlA.AfterItem ] score
]
, menu
, Html.div [] [] |> menu
]
)
@@ -994,33 +1056,33 @@ userDetails shared game userId user =
game |> Maybe.map .round
details =
[ Strings.Privileged |> Maybe.justIf (user.privilege == User.Privileged)
, Strings.Ai |> Maybe.justIf (user.control == User.Computer)
, round |> Maybe.andThen (\r -> Strings.Czar |> Maybe.justIf (Player.isCzar r userId))
, Strings.Disconnected |> Maybe.justIf (user.connection == User.Disconnected && user.presence /= User.Left)
, Strings.Away |> Maybe.justIf ((player |> Maybe.map .presence) == Just Player.Away)
[ ( "privileged", Strings.Privileged ) |> Maybe.justIf (user.privilege == User.Privileged)
, ( "ai", Strings.Ai ) |> Maybe.justIf (user.control == User.Computer)
, round |> Maybe.andThen (\r -> ( "czar", Strings.Czar ) |> Maybe.justIf (Player.isCzar r userId))
, ( "disconnected", Strings.Disconnected ) |> Maybe.justIf (user.connection == User.Disconnected && user.presence /= User.Left)
, ( "away", Strings.Away ) |> Maybe.justIf ((player |> Maybe.map .presence) == Just Player.Away)
, playStateDetail round userId
]
score =
player |> Maybe.map (\p -> Strings.Score { total = p.score })
player |> Maybe.map (\p -> ( "score", Strings.Score { total = p.score } ))
likes =
player |> Maybe.map (\p -> Strings.Likes { total = p.likes })
player |> Maybe.map (\p -> ( "likes", Strings.Likes { total = p.likes } ))
in
( viewDetails shared details, viewDetails shared [ score, likes ] )
playStateDetail : Maybe Round -> User.Id -> Maybe MdString
playStateDetail : Maybe Round -> User.Id -> Maybe ( String, MdString )
playStateDetail round userId =
case round of
Just (Round.P p) ->
case Player.playState p userId of
Player.Playing ->
Just Strings.StillPlaying
Just ( "playing", Strings.StillPlaying )
Player.Played ->
Just Strings.Played
Just ( "played", Strings.Played )
Player.NotInRound ->
Nothing
@@ -1029,20 +1091,18 @@ playStateDetail round userId =
Nothing
viewDetails : Shared -> List (Maybe MdString) -> List (Html msg)
viewDetails shared details =
details |> List.filterMap identity |> List.map (Lang.html shared) |> List.intersperse (Html.text " ")
viewDetails : Shared -> List (Maybe ( String, MdString )) -> List (Html msg)
viewDetails shared =
List.filterMap identity
>> List.map (\( cls, str ) -> Html.span [ HtmlA.class cls ] [ str |> Lang.html shared ])
viewCastButton : (Msg -> msg) -> Auth -> List (Html.Attribute msg) -> List (Html msg)
viewCastButton wrap auth attrs =
[ Components.iconButtonStyled
(List.concat
[ [ HtmlA.class "cast-button"
, auth |> TryCast |> wrap |> HtmlE.onClick
]
, attrs
]
)
( [ Icon.lg ], Icon.chromecast )
viewCastButton : (Msg -> msg) -> Shared -> Auth -> List (Html.Attribute msg) -> List (Html msg)
viewCastButton wrap shared auth attrs =
[ Html.div (HtmlA.class "cast-button" :: attrs)
[ IconButton.view shared
Strings.Cast
(Icon.chromecast |> Icon.present |> Icon.styled [ Icon.lg ] |> NeList.just)
(auth |> TryCast |> wrap |> Just)
]
]
@@ -15,9 +15,7 @@ import Json.Decode as Json
import Json.Diff as Json
import Json.Encode
import Json.Patch as Json
import MassiveDecks.Card.Source.Cardcast.Model as Cardcast
import MassiveDecks.Card.Source.Model as Source exposing (Source)
import MassiveDecks.Components as Components
import MassiveDecks.Card.Source.Model exposing (Source)
import MassiveDecks.Components.Form as Form
import MassiveDecks.Components.Form.Message as Message exposing (Message)
import MassiveDecks.Error.Model as Error exposing (Error)
@@ -52,8 +50,13 @@ import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.User as User exposing (User)
import MassiveDecks.Util.Html as Html
import MassiveDecks.Util.Maybe as Maybe
import Weightless as Wl
import Weightless.Attributes as WlA
import MassiveDecks.Util.NeList exposing (NeList(..))
import Material.Attributes as Material
import Material.Button as Button
import Material.Card as Card
import Material.Fab as Fab
import Material.Tabs as Tabs
import Material.TextField as TextField
init : Shared -> Model
@@ -177,16 +180,19 @@ update shared msg model config =
( model, shared, Cmd.none )
view : (Msg -> msg) -> (Lobby.Msg -> msg) -> Shared -> Maybe msg -> Bool -> Model -> GameCode -> Lobby -> Html msg
view wrap wrapLobby shared return canEdit model gameCode lobby =
view : (Msg -> msg) -> (Lobby.Msg -> msg) -> Shared -> Maybe msg -> Message msg -> Model -> GameCode -> Lobby -> Html msg
view wrap wrapLobby shared return disabledReason model gameCode lobby =
let
canEdit =
disabledReason |> Maybe.isNothing
conflicts =
if List.isEmpty model.conflicts then
Html.nothing
else
Html.div [ HtmlA.id "merge-overlay" ]
[ Wl.card []
[ Card.view []
[ model.conflicts |> viewMerge wrap shared model model.localConfig lobby.config (wrap NoOp) canEdit
]
]
@@ -205,27 +211,37 @@ view wrap wrapLobby shared return canEdit model gameCode lobby =
Privacy ->
Privacy.All |> PrivacyId
returnButton msg =
Wl.card []
[ Wl.button [ HtmlA.class "game-in-progress", HtmlE.onClick msg, Strings.ReturnViewToGameDescription |> Lang.title shared ]
[ Icon.play |> Icon.viewIcon
, Strings.ReturnViewToGame |> Lang.html shared
]
]
viewReturnButton msg =
Button.view shared
Button.Raised
Strings.ReturnViewToGame
Strings.ReturnViewToGameDescription
(Icon.arrowLeft |> Icon.viewIcon)
[ HtmlA.class "game-in-progress", HtmlE.onClick msg ]
returnButton =
return |> Maybe.map viewReturnButton |> Maybe.withDefault (Html.div [] [])
in
Html.div [ HtmlA.class "configure" ]
[ return |> Maybe.map returnButton |> Maybe.withDefault Html.nothing
, Wl.card []
[ Card.view []
[ Html.div [ HtmlA.class "title" ]
[ Component.view (componentById NameId) wrap shared model model.localConfig lobby.config (wrap NoOp) canEdit ConfigOption.Local |> Maybe.withDefault Html.nothing
, Html.div [ HtmlA.class "joining" ]
[ Invite.button wrapLobby shared
, Strings.GameCode { code = GameCode.toString gameCode } |> Lang.html shared
, Html.label [ Lobby.ToggleInviteDialog |> wrapLobby |> HtmlE.onClick ]
[ Strings.GameCode { code = GameCode.toString gameCode } |> Lang.html shared ]
]
]
]
, Wl.card []
[ Wl.tabGroup [ WlA.align WlA.Center ] (tabs |> List.map (tab wrap shared model.tab))
, disabledReason
|> Message.view shared
|> Maybe.withDefault Html.nothing
, Tabs.view shared
{ selected = model.tab
, change = ChangeTab >> wrap
, ids = tabs
, tab = tab
, equals = (==)
}
, Component.view (componentById tabComponent)
wrap
shared
@@ -236,9 +252,7 @@ view wrap wrapLobby shared return canEdit model gameCode lobby =
canEdit
ConfigOption.Local
|> Maybe.withDefault Html.nothing
]
, Wl.card []
[ startGameSegment wrap wrapLobby shared canEdit model lobby
, startGameSegment wrap wrapLobby shared canEdit model lobby returnButton
]
, actions wrap shared (model.localConfig /= lobby.config)
, conflicts
@@ -270,19 +284,18 @@ applyChange change config model =
actions wrap shared hasChanges =
Html.div [ HtmlA.class "actions" ]
[ Components.floatingActionButton
[ Strings.SaveChanges |> Lang.title shared
, SaveChanges |> wrap |> HtmlE.onClick
, HtmlA.classList [ ( "action", True ), ( "important", True ), ( "exited", not hasChanges ) ]
]
Icon.save
, Components.floatingActionButton
[ Strings.RevertChanges |> Lang.title shared
, RevertChanges |> wrap |> HtmlE.onClick
, HtmlA.classList [ ( "action", True ), ( "exited", not hasChanges ) ]
, WlA.inverted
]
Icon.undo
[ Fab.view shared
Fab.Normal
Strings.SaveChanges
(Icon.save |> Icon.present)
(SaveChanges |> wrap |> Just)
[ HtmlA.classList [ ( "action", True ), ( "important", True ), ( "exited", not hasChanges ) ] ]
, Fab.view shared
Fab.Mini
Strings.RevertChanges
(Icon.undo |> Icon.present)
(RevertChanges |> wrap |> Just)
[ HtmlA.classList [ ( "action", True ), ( "exited", not hasChanges ), ( "normal", True ) ] ]
]
@@ -329,8 +342,14 @@ viewConflict wrap shared model local config noOp canEdit conflict =
]
resolveButton : (Msg -> msg) -> Shared -> Id -> Config.Source -> MdString -> Html msg
resolveButton wrap shared conflict source description =
Wl.button [ HtmlA.class "resolve", ResolveConflict source conflict |> wrap |> HtmlE.onClick ] [ description |> Lang.html shared ]
Button.view shared
Button.Raised
description
description
Html.nothing
[ HtmlA.class "resolve", ResolveConflict source conflict |> wrap |> HtmlE.onClick ]
default : Config
@@ -408,12 +427,12 @@ nameOption =
\_ ->
ConfigOption.TextField
{ placeholder = Strings.LobbyNameLabel
, inputType = Nothing
, inputType = TextField.Text
, toString = Just
, fromString = Just
, attrs = [ WlA.minLength 1, WlA.maxLength 100 ]
, attrs = [ Material.minLength 1, Material.maxLength 100 ]
}
, extraEditor = \_ -> \_ -> \_ -> Nothing
, extraEditor = \_ -> \_ -> \_ -> \_ -> Nothing
, set = ConfigOption.wrappedSetter NameChange
, messages = \_ -> []
}
@@ -436,8 +455,8 @@ mergeChange base local remote model =
{ model | localConfig = updated, conflicts = oldConflicts ++ newConflicts }
startGameSegment : (Msg -> msg) -> (Lobby.Msg -> msg) -> Shared -> Bool -> Model -> Lobby -> Html msg
startGameSegment wrap wrapLobby shared canEdit model lobby =
startGameSegment : (Msg -> msg) -> (Lobby.Msg -> msg) -> Shared -> Bool -> Model -> Lobby -> Html msg -> Html msg
startGameSegment wrap wrapLobby shared canEdit model lobby returnButton =
let
config =
lobby.config
@@ -447,15 +466,27 @@ startGameSegment wrap wrapLobby shared canEdit model lobby =
startGameAttrs =
if List.isEmpty startErrors && canEdit then
[ StartGame |> wrap |> HtmlE.onClick ]
StartGame |> wrap |> HtmlE.onClick
else
[ WlA.disabled ]
HtmlA.disabled True
in
Form.section shared
"start-game"
(Wl.button startGameAttrs [ Strings.StartGame |> Lang.html shared ])
(startErrors |> Maybe.justIf canEdit |> Maybe.withDefault [])
Html.div []
[ Form.section
shared
"start-game"
Html.nothing
(startErrors |> Maybe.justIf canEdit |> Maybe.withDefault [])
, Html.div [ HtmlA.class "button-spread" ]
[ returnButton
, Button.view shared
Button.Raised
Strings.StartGame
Strings.StartGame
(Icon.rocket |> Icon.viewIcon)
[ startGameAttrs ]
]
]
startGameProblems : Shared -> (Msg -> msg) -> (Lobby.Msg -> msg) -> Dict User.Id User -> Model -> Config -> List (Message msg)
@@ -496,7 +527,7 @@ startGameProblems shared wrap wrapLobby users model remote =
summaries .responses
requiredResponses =
(users |> Dict.values |> List.filter (\u -> u.role == User.Player) |> List.length) * 3
(users |> Dict.values |> List.filter (\u -> u.role == User.Player) |> List.length) * 3 * 2
deckIssues =
if noDecks then
@@ -514,29 +545,37 @@ startGameProblems shared wrap wrapLobby users model remote =
[ Strings.WaitForDecks |> Message.info |> Just ]
else
let
diff =
requiredResponses - numberOfResponses
old =
config.rules.houseRules.comedyWriter |> Maybe.map .number |> Maybe.withDefault 0
fixMsg =
diff + old |> Just |> ComedyWriter.SetNumber |> HouseRule.ComedyWriterMsg |> Rules.HouseRulesMsg |> RulesMsg |> wrap
in
[ Strings.MissingCardType { cardType = Strings.Call }
|> Message.error
|> Maybe.justIf (summaries .calls < 1)
, Strings.MissingCardType { cardType = Strings.Response }
|> Message.error
|> Maybe.justIf (numberOfResponses < 1)
, Strings.NotEnoughCardsOfType { cardType = Strings.Response, needed = requiredResponses, have = numberOfResponses }
|> Message.error
, Message.errorWithFix
(Strings.NotEnoughCardsOfType { cardType = Strings.Response, needed = requiredResponses, have = numberOfResponses })
[ Message.Fix (Strings.AddBlankCards { amount = diff }) Icon.plus fixMsg ]
|> Maybe.justIf (numberOfResponses < requiredResponses)
]
playerCount =
users
|> Dict.values
|> List.filter (\user -> user.role == User.Player && user.presence == User.Joined)
|> List.length
humanPlayerCount =
users
|> Dict.values
|> List.filter (\user -> user.role == User.Player && user.presence == User.Joined && user.control == User.Human)
|> List.filter (\u -> u.role == User.Player && u.presence == User.Joined && u.control == User.Human)
|> List.length
computerPlayers =
config.rules.houseRules.rando |> Maybe.map .number |> Maybe.withDefault 0
playerCount =
humanPlayerCount + computerPlayers
playerIssues =
[ Message.errorWithFix
Strings.NeedAtLeastThreePlayers
@@ -614,18 +653,16 @@ startGameProblems shared wrap wrapLobby users model remote =
[ deckIssues, playerIssues, aisNoWriteGoodIssues, configurationIssues ] |> List.concat |> List.filterMap identity
tabs : List Tab
tabs : NeList Tab
tabs =
[ Decks, Rules, TimeLimits, Privacy ]
NeList Decks [ Rules, TimeLimits, Privacy ]
tab : (Msg -> msg) -> Shared -> Tab -> Tab -> Html msg
tab wrap shared currently target =
Wl.tab
((target |> ChangeTab |> wrap |> always |> HtmlE.onCheck)
:: ([ WlA.checked ] |> Maybe.justIf (currently == target) |> Maybe.withDefault [])
)
[ target |> tabName |> Lang.html shared ]
tab : Tab -> Tabs.TabModel
tab target =
{ label = target |> tabName
, icon = Nothing
}
tabName : Tab -> MdString
@@ -1,11 +1,14 @@
module MassiveDecks.Pages.Lobby.Configure.ConfigOption exposing
( ConfigOption
, IntBounds
, PrimaryEditor(..)
, RenderMode(..)
, ViewArgs
, intEditor
, maybeEditor
, noExtraEditor
, toMinMaxAttrs
, toValidator
, view
, wrappedSetter
)
@@ -16,14 +19,13 @@ import Html.Events as HtmlE
import MassiveDecks.Components.Form as Form
import MassiveDecks.Components.Form.Message exposing (Message)
import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Pages.Lobby.Configure.Component.Validator exposing (Validator)
import MassiveDecks.Pages.Lobby.Configure.Component.Validator as Validator exposing (Validator)
import MassiveDecks.Pages.Lobby.Configure.ConfigOption.Toggleable exposing (Toggleable)
import MassiveDecks.Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Html.Attributes as HtmlA
import MassiveDecks.Util.Maybe as Maybe
import Weightless as Wl
import Weightless.Attributes as WlA
import Material.Switch as Material
import Material.TextField as TextField
type alias Model model config =
@@ -34,7 +36,7 @@ type alias ConfigOption model config iMsg msg =
{ id : String
, toggleable : Toggleable config
, primaryEditor : model -> PrimaryEditor config msg
, extraEditor : (iMsg -> msg) -> model -> config -> Maybe (Html msg)
, extraEditor : (iMsg -> msg) -> Shared -> model -> config -> Maybe (Html msg)
, set : (iMsg -> msg) -> config -> msg
, messages : (iMsg -> msg) -> List (Message msg)
}
@@ -43,7 +45,7 @@ type alias ConfigOption model config iMsg msg =
type PrimaryEditor value msg
= TextField
{ placeholder : MdString
, inputType : Maybe WlA.InputType
, inputType : TextField.Type
, toString : value -> Maybe String
, fromString : String -> Maybe value
, attrs : List (Html.Attribute msg)
@@ -55,7 +57,7 @@ intEditor : MdString -> List (Html.Attribute msg) -> PrimaryEditor Int msg
intEditor placeholder attrs =
TextField
{ placeholder = placeholder
, inputType = Just WlA.Number
, inputType = TextField.Number
, toString = String.fromInt >> Just
, fromString = String.toInt
, attrs = attrs
@@ -78,9 +80,9 @@ maybeEditor given =
Label { text = text }
noExtraEditor : (iMsg -> msg) -> model -> config -> Maybe (Html msg)
noExtraEditor =
\_ -> \_ -> \_ -> Nothing
noExtraEditor : (iMsg -> msg) -> Shared -> model -> config -> Maybe (Html msg)
noExtraEditor _ _ _ _ =
Nothing
wrappedSetter : (config -> iMsg) -> (iMsg -> msg) -> config -> msg
@@ -144,6 +146,30 @@ view opt validator { wrap, shared, model, local, remote, noOp, canEdit, renderMo
(errors ++ opt.messages wrap)
{-| Boundaries for an integer value.
-}
type alias IntBounds =
{ min : Int
, max : Int
}
{-| Convert bounds to min/max HTML attributes.
-}
toMinMaxAttrs : IntBounds -> List (Html.Attribute msg)
toMinMaxAttrs { min, max } =
[ min |> String.fromInt |> HtmlA.min
, max |> String.fromInt |> HtmlA.max
]
{-| Convert bounds to a validator.
-}
toValidator : IntBounds -> (Int -> iMsg) -> Validator Int iMsg msg
toValidator { min, max } =
Validator.between min max
{- Private -}
@@ -151,54 +177,73 @@ view opt validator { wrap, shared, model, local, remote, noOp, canEdit, renderMo
viewContents : ConfigOption model config optionMsg msg -> (optionMsg -> msg) -> Shared -> model -> msg -> Bool -> Bool -> config -> List (Html msg)
viewContents opt wrap shared model noOp canEdit readOnly config =
let
( switch, toggle, id ) =
case opt.toggleable of
Just { off, on } ->
let
i =
opt.id ++ "-swtich"
t =
Maybe.justIf (config == off) on |> Maybe.withDefault off |> opt.set wrap
s =
Material.view
[ HtmlA.checked (config /= off)
, if readOnly || not canEdit then
HtmlA.disabled True
else
HtmlE.onCheck (\c -> Maybe.justIf c on |> Maybe.withDefault off |> opt.set wrap)
, HtmlA.id i
]
in
( Just s, t |> Maybe.justIf (not readOnly && canEdit), Just i )
Nothing ->
( Nothing, Nothing, Nothing )
primaryEditor =
case opt.primaryEditor model of
TextField { placeholder, inputType, toString, fromString, attrs } ->
Wl.textField
([ HtmlA.class "primary"
, placeholder |> Lang.label shared
, WlA.outlined
, inputType |> Maybe.map WlA.type_ |> Maybe.withDefault HtmlA.nothing
, config |> toString |> Maybe.map WlA.value |> Maybe.withDefault HtmlA.nothing
, if readOnly then
WlA.readonly
let
toggleableAndOff =
opt.toggleable |> Maybe.map (.off >> (==) config) |> Maybe.withDefault False
in
TextField.view shared
placeholder
inputType
(config |> toString |> Maybe.withDefault "")
(List.filterMap identity
[ HtmlA.class "primary" |> Just
, if readOnly then
HtmlA.readonly True |> Just
else
fromString
>> Maybe.map (opt.set wrap)
>> Maybe.withDefault noOp
|> HtmlE.onInput
, opt.toggleable
|> Maybe.andThen (\t -> Maybe.justIf (t.off == config) WlA.disabled)
|> Maybe.withDefault HtmlA.nothing
, WlA.disabled |> Maybe.justIf (not canEdit) |> Maybe.withDefault HtmlA.nothing
]
else
fromString
>> Maybe.map (opt.set wrap)
>> Maybe.withDefault noOp
|> HtmlE.onInput
|> Just
, HtmlA.disabled True |> Maybe.justIf toggleableAndOff
, toggle |> Maybe.andThen (Maybe.justIf toggleableAndOff) |> Maybe.map HtmlE.onClick
, True |> HtmlA.disabled |> Maybe.justIf (not canEdit)
]
++ attrs
)
[]
Label { text } ->
Html.label [ HtmlA.class "primary" ] [ text |> Lang.html shared ]
switch =
case opt.toggleable of
Just { off, on } ->
Wl.switch
[ WlA.checked |> Maybe.justIf (config /= off) |> Maybe.withDefault HtmlA.nothing
, if readOnly then
WlA.disabled
else
HtmlE.onCheck (\c -> Maybe.justIf c on |> Maybe.withDefault off |> opt.set wrap)
, WlA.disabled |> Maybe.justIf (not canEdit) |> Maybe.withDefault HtmlA.nothing
]
|> Just
Nothing ->
Nothing
Html.label
(List.filterMap identity
[ HtmlA.class "primary" |> Just
, toggle |> Maybe.map HtmlE.onClick
, id |> Maybe.map HtmlA.for
]
)
[ text |> Lang.html shared ]
in
[ switch
, Just primaryEditor
, opt.extraEditor wrap model config
, opt.extraEditor wrap shared model config
]
|> List.filterMap identity
@@ -30,9 +30,11 @@ import MassiveDecks.Settings as Settings
import MassiveDecks.Strings as Strings
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Html as Html
import MassiveDecks.Util.Html.Attributes as HtmlA
import MassiveDecks.Util.Maybe as Maybe
import MassiveDecks.Util.NeList exposing (NeList(..))
import MassiveDecks.Util.Result as Result
import Material.IconButton as IconButton
import Paper.Tooltip as Tooltip
import Svg.Attributes as SvgA
@@ -74,6 +76,9 @@ update shared msg model =
Remove index ->
( model, shared, Actions.configure (removeDeck index) )
NoOp ->
( model, shared, Cmd.none )
{-| View the editor/viewer for decks.
-}
@@ -83,8 +88,7 @@ view _ { wrap, shared, model, remote, canEdit } =
hint =
if canEdit then
Components.linkButton
[ shared |> Lang.recommended |> Add |> wrap |> HtmlE.onClick
]
[ shared |> Lang.recommended |> Add |> wrap |> HtmlE.onClick ]
[ Strings.NoDecksHint |> Lang.html shared ]
else
@@ -138,7 +142,7 @@ view _ { wrap, shared, model, remote, canEdit } =
addDeck : Source.External -> Json.Patch
addDeck source =
[ Json.Add [ "decks", "-" ] (D (Deck source Nothing) |> Encoders.deckOrError) ]
[ Json.Add [ "decks", "-" ] (DeckOrError source (Ok Nothing) |> Encoders.deckOrError) ]
removeDeck : Int -> Json.Patch
@@ -175,21 +179,21 @@ all =
getDeck : DeckOrError -> Maybe Deck
getDeck deckOrError =
case deckOrError of
D deck ->
Just deck
case deckOrError.result of
Ok summary ->
Deck deckOrError.source summary |> Just
E _ ->
Err _ ->
Nothing
summaryFor : DeckOrError -> Maybe ( Source.External, Source.Summary )
summaryFor deckOrError =
case deckOrError of
D deck ->
deck.summary |> Maybe.map (\s -> ( deck.source, s ))
case deckOrError.result of
Ok summary ->
summary |> Maybe.map (\s -> ( deckOrError.source, s ))
E _ ->
Err _ ->
Nothing
@@ -199,33 +203,28 @@ addDeckWidget wrap shared existing deckToAdd =
submit =
deckToAdd |> submitDeckAction wrap existing
in
Html.form
[ submit |> Result.map HtmlE.onSubmit |> Result.withDefault HtmlA.nothing ]
[ Form.section
shared
"add-deck"
(Html.div [ HtmlA.class "multipart" ]
(List.concat
[ Source.generalEditor shared existing deckToAdd (Update >> wrap)
, [ Components.floatingActionButton
[ HtmlA.type_ "submit"
, Result.isError submit |> HtmlA.disabled
, Strings.AddDeck |> Lang.title shared
]
Icon.plus
]
]
)
Form.section
shared
"add-deck"
(Html.div [ HtmlA.class "multipart" ]
(List.concat
[ Source.generalEditor shared existing deckToAdd (Update >> wrap) (submit |> Result.toMaybe) (NoOp |> wrap)
, [ IconButton.view shared
Strings.AddDeck
(NeList (Icon.plus |> Icon.present) [])
(submit |> Result.toMaybe)
]
]
)
(submit |> Result.error |> Maybe.withDefault [])
]
)
(submit |> Result.error |> Maybe.withDefault [])
submitDeckAction : (Msg -> msg) -> List DeckOrError -> Source.External -> Result (List (Message msg)) msg
submitDeckAction wrap existing deckToAdd =
let
potentialProblems =
if List.any (getSource >> Source.equals deckToAdd) existing then
if List.any (.source >> Source.equals deckToAdd) existing then
[ Strings.DeckAlreadyAdded |> Message.info ]
else
@@ -238,26 +237,19 @@ submitDeckAction wrap existing deckToAdd =
potentialProblems |> Result.Err
getSource : DeckOrError -> Source.External
getSource deckOrError =
case deckOrError of
D deck ->
deck.source
E error ->
error.source
viewDeck : (Msg -> msg) -> Shared -> Bool -> Int -> DeckOrError -> Html msg
viewDeck wrap shared canEdit index deckOrError =
let
( deckSource, deckSummary, failureReason ) =
case deckOrError of
D { source, summary } ->
( source, summary, Nothing )
deckSource =
deckOrError.source
E { source, reason } ->
( source, Nothing, Just reason )
( deckSummary, failureReason ) =
case deckOrError.result of
Ok summary ->
( summary, Nothing )
Err reason ->
( Nothing, Just reason )
( attr, columns ) =
case deckSummary of
@@ -299,16 +291,15 @@ name :
name wrap shared canEdit index source loading maybeError details =
let
removeButton =
Components.iconButton
[ index |> Remove |> wrap |> HtmlE.onClick
, Strings.RemoveDeck |> Lang.title shared
, HtmlA.class "remove-button"
]
Icon.trash
IconButton.view
shared
Strings.RemoveDeck
(NeList (Icon.trash |> Icon.present) [])
(index |> Remove |> wrap |> Just)
|> Maybe.justIf canEdit
( maybeId, tooltip ) =
source |> Source.Ex |> Source.tooltip shared |> Maybe.decompose
source |> Source.Ex |> Source.tooltip shared Tooltip.Right details |> Maybe.decompose
attrs =
maybeId |> Maybe.map (\id -> [ HtmlA.id id ]) |> Maybe.withDefault []
@@ -1,12 +1,14 @@
module MassiveDecks.Pages.Lobby.Configure.Decks.Model exposing
( Config
, Deck
, DeckOrError(..)
, DeckOrError
, Error
, GetSummary
, Id(..)
, Model
, Msg(..)
, deckToDeckOrError
, errorToDeckOrError
)
import MassiveDecks.Card.Source.Model as Source exposing (Source)
@@ -36,9 +38,20 @@ type alias Error =
{-| A deck or an error.
-}
type DeckOrError
= D Deck
| E Error
type alias DeckOrError =
{ source : Source.External
, result : Result Source.LoadFailureReason (Maybe Source.Summary)
}
deckToDeckOrError : Deck -> DeckOrError
deckToDeckOrError { source, summary } =
{ source = source, result = Ok summary }
errorToDeckOrError : Error -> DeckOrError
errorToDeckOrError { source, reason } =
{ source = source, result = Err reason }
{-| The model for the editor for decks.
@@ -60,3 +73,4 @@ type Msg
= Update Source.External
| Add Source.External
| Remove Int
| NoOp
@@ -5,9 +5,8 @@ module MassiveDecks.Pages.Lobby.Configure.Privacy exposing
, update
)
import FontAwesome.Icon as Icon
import FontAwesome.Solid as Icon
import Html.Events as HtmlE
import MassiveDecks.Components as Components
import MassiveDecks.Components.Form.Message as Message
import MassiveDecks.Pages.Lobby.Configure.Component as Component exposing (Component)
import MassiveDecks.Pages.Lobby.Configure.Component.Validator as Validator
@@ -15,9 +14,9 @@ import MassiveDecks.Pages.Lobby.Configure.ConfigOption as ConfigOption exposing
import MassiveDecks.Pages.Lobby.Configure.ConfigOption.Toggleable as Toggleable
import MassiveDecks.Pages.Lobby.Configure.Privacy.Model exposing (..)
import MassiveDecks.Strings as Strings
import MassiveDecks.Util.Html.Attributes as HtmlA
import MassiveDecks.Util.Maybe as Maybe
import Weightless.Attributes as WlA
import MassiveDecks.Util.NeList as NeList exposing (NeList(..))
import Material.IconButton as IconButton
import Material.TextField as TextField
init : Model
@@ -105,28 +104,38 @@ password =
passwordOption : ConfigOption Model (Maybe String) Msg msg
passwordOption =
let
visibility wrap shared model _ =
let
icon =
if model.passwordVisible then
Icon.eyeSlash
else
Icon.eye
action =
TogglePasswordVisibility |> wrap |> Just
in
Just (IconButton.view shared Strings.LobbyPassword (icon |> Icon.present |> NeList.just) action)
in
{ id = "game-password-option"
, toggleable = Toggleable.maybe ""
, primaryEditor =
\model ->
ConfigOption.TextField
{ placeholder = Strings.LobbyPassword
, inputType = WlA.Password |> Maybe.justIf (not model.passwordVisible)
, inputType =
if not model.passwordVisible then
TextField.Password
else
TextField.Text
, toString = identity
, fromString = Just >> Just
, attrs = [ WlA.minLength 1 ]
, attrs = []
}
, extraEditor =
\wrap ->
\model ->
\value ->
Just
(Components.iconButton
[ TogglePasswordVisibility |> wrap |> HtmlE.onClick
, WlA.disabled |> Maybe.justIf (Maybe.isNothing value) |> Maybe.withDefault HtmlA.nothing
]
(Icon.eyeSlash |> Maybe.justIf model.passwordVisible |> Maybe.withDefault Icon.eye)
)
, extraEditor = visibility
, set = ConfigOption.wrappedSetter PasswordChange
, messages =
\_ ->
@@ -13,7 +13,6 @@ import MassiveDecks.Pages.Lobby.Configure.Rules.HouseRules as HouseRules
import MassiveDecks.Pages.Lobby.Configure.Rules.HouseRules.Model as HouseRules
import MassiveDecks.Pages.Lobby.Configure.Rules.Model exposing (..)
import MassiveDecks.Strings as Strings
import Weightless.Attributes as WlA
init : Model
@@ -81,13 +80,18 @@ gameRules =
]
handSizeLimits : ConfigOption.IntBounds
handSizeLimits =
ConfigOption.IntBounds 3 50
handSize : Component Int Model Id Msg msg
handSize =
Component.value
HandSize
(ConfigOption.view handSizeOption)
(always False)
(Validator.between 3 50 HandSizeChange)
(ConfigOption.toValidator handSizeLimits HandSizeChange)
handSizeOption : ConfigOption Model Int Msg msg
@@ -95,20 +99,25 @@ handSizeOption =
{ id = "hand-size-option"
, toggleable = Nothing
, primaryEditor =
\_ -> ConfigOption.intEditor Strings.HandSize [ 3 |> WlA.min, 50 |> WlA.max ]
\_ -> ConfigOption.intEditor Strings.HandSize (ConfigOption.toMinMaxAttrs handSizeLimits)
, extraEditor = ConfigOption.noExtraEditor
, set = ConfigOption.wrappedSetter HandSizeChange
, messages = \_ -> [ Message.info Strings.HandSizeDescription ]
}
scoreLimitBounds : ConfigOption.IntBounds
scoreLimitBounds =
ConfigOption.IntBounds 1 10000
scoreLimit : Component (Maybe Int) Model Id Msg msg
scoreLimit =
Component.value
ScoreLimit
(ConfigOption.view scoreLimitOption)
(always False)
(Validator.optional (Validator.between 3 10000 (Just >> ScoreLimitChange)))
(Validator.optional (ConfigOption.toValidator scoreLimitBounds (Just >> ScoreLimitChange)))
scoreLimitOption : ConfigOption Model (Maybe Int) Msg msg
@@ -116,7 +125,7 @@ scoreLimitOption =
{ id = "score-limit-option"
, toggleable = Toggleable.maybe 25
, primaryEditor =
\_ -> ConfigOption.maybeEditor (ConfigOption.intEditor Strings.ScoreLimit [ 3 |> WlA.min, 10000 |> WlA.max ])
\_ -> ConfigOption.maybeEditor (ConfigOption.intEditor Strings.ScoreLimit (ConfigOption.toMinMaxAttrs scoreLimitBounds))
, extraEditor = ConfigOption.noExtraEditor
, set = ConfigOption.wrappedSetter ScoreLimitChange
, messages = \_ -> [ Message.info Strings.ScoreLimitDescription ]
@@ -14,7 +14,6 @@ import MassiveDecks.Pages.Lobby.Configure.ConfigOption.Toggleable as Toggleable
import MassiveDecks.Pages.Lobby.Configure.Rules.HouseRules.ComedyWriter.Model exposing (..)
import MassiveDecks.Strings as Strings
import MassiveDecks.Util.Maybe as Maybe
import Weightless.Attributes as WlA
init : Model
@@ -115,14 +114,9 @@ enabledOption =
}
numberMin : Int
numberMin =
1
numberMax : Int
numberMax =
99999
numberBounds : ConfigOption.IntBounds
numberBounds =
ConfigOption.IntBounds 1 99999
number : Component (Maybe Int) Model Id Msg msg
@@ -131,7 +125,7 @@ number =
Number
(ConfigOption.view numberOption)
Maybe.isNothing
(Validator.optional (Validator.between numberMin numberMax (Just >> SetNumber)))
(Validator.optional (ConfigOption.toValidator numberBounds (Just >> SetNumber)))
numberOption : ConfigOption Model (Maybe Int) Msg msg
@@ -140,7 +134,7 @@ numberOption =
, toggleable = Nothing
, primaryEditor =
\_ ->
ConfigOption.intEditor Strings.HouseRuleComedyWriterNumber [ WlA.min numberMin, WlA.max numberMax ]
ConfigOption.intEditor Strings.HouseRuleComedyWriterNumber (ConfigOption.toMinMaxAttrs numberBounds)
|> ConfigOption.maybeEditor
, extraEditor = ConfigOption.noExtraEditor
, set = ConfigOption.wrappedSetter SetNumber
@@ -14,7 +14,6 @@ import MassiveDecks.Pages.Lobby.Configure.ConfigOption.Toggleable as Toggleable
import MassiveDecks.Pages.Lobby.Configure.Rules.HouseRules.Rando.Model exposing (..)
import MassiveDecks.Strings as Strings
import MassiveDecks.Util.Maybe as Maybe
import Weightless.Attributes as WlA
init : Model
@@ -102,14 +101,9 @@ enabledOption =
}
numberMin : Int
numberMin =
1
numberMax : Int
numberMax =
10
numberBounds : ConfigOption.IntBounds
numberBounds =
ConfigOption.IntBounds 1 10
number : Component (Maybe Int) Model Id Msg msg
@@ -118,7 +112,7 @@ number =
Number
(ConfigOption.view numberOption)
Maybe.isNothing
(Validator.optional (Validator.between numberMin numberMax (Just >> SetNumber)))
(Validator.optional (ConfigOption.toValidator numberBounds (Just >> SetNumber)))
numberOption : ConfigOption Model (Maybe Int) Msg msg
@@ -127,7 +121,7 @@ numberOption =
, toggleable = Toggleable.none
, primaryEditor =
\_ ->
ConfigOption.intEditor Strings.HouseRuleRandoCardrissianNumber [ WlA.min numberMin, WlA.max numberMax ]
ConfigOption.intEditor Strings.HouseRuleRandoCardrissianNumber (ConfigOption.toMinMaxAttrs numberBounds)
|> ConfigOption.maybeEditor
, extraEditor = ConfigOption.noExtraEditor
, set = ConfigOption.wrappedSetter SetNumber
@@ -14,7 +14,6 @@ import MassiveDecks.Pages.Lobby.Configure.ConfigOption.Toggleable as Toggleable
import MassiveDecks.Pages.Lobby.Configure.Rules.HouseRules.Reboot.Model exposing (..)
import MassiveDecks.Strings as Strings
import MassiveDecks.Util.Maybe as Maybe
import Weightless.Attributes as WlA
init : Model
@@ -100,14 +99,9 @@ enabledOption =
}
costMin : Int
costMin =
1
costMax : Int
costMax =
50
costBounds : ConfigOption.IntBounds
costBounds =
ConfigOption.IntBounds 1 50
cost : Component (Maybe Int) Model Id Msg msg
@@ -116,7 +110,7 @@ cost =
Cost
(ConfigOption.view costOption)
Maybe.isNothing
(Validator.optional (Validator.between costMin costMax (Just >> SetCost)))
(Validator.optional (ConfigOption.toValidator costBounds (Just >> SetCost)))
costOption : ConfigOption Model (Maybe Int) Msg msg
@@ -125,7 +119,7 @@ costOption =
, toggleable = Toggleable.none
, primaryEditor =
\_ ->
ConfigOption.intEditor Strings.HouseRuleRebootCost [ WlA.min costMin, WlA.max costMax ]
ConfigOption.intEditor Strings.HouseRuleRebootCost (ConfigOption.toMinMaxAttrs costBounds)
|> ConfigOption.maybeEditor
, extraEditor = ConfigOption.noExtraEditor
, set = ConfigOption.wrappedSetter SetCost
@@ -14,7 +14,6 @@ import MassiveDecks.Pages.Lobby.Configure.ConfigOption as ConfigOption exposing
import MassiveDecks.Pages.Lobby.Configure.TimeLimits.Model exposing (..)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Util.Maybe as Maybe
import Weightless.Attributes as WlA
init : Model
@@ -101,13 +100,18 @@ modeOption =
}
timeLimitBounds : ConfigOption.IntBounds
timeLimitBounds =
ConfigOption.IntBounds 0 900
timeLimit : Round.Stage -> MdString -> Bool -> Component (Maybe Int) Model Id Msg msg
timeLimit stage description toggleable =
Component.value
(TimeLimit stage)
(ConfigOption.view (timeLimitOption stage description toggleable))
(always False)
(Validator.optional (Validator.between 0 900 (\v -> TimeLimitChange stage (Just v))))
(Validator.optional (ConfigOption.toValidator timeLimitBounds (\v -> TimeLimitChange stage (Just v))))
stageId : Round.Stage -> String
@@ -135,7 +139,7 @@ timeLimitOption stage description toggleable =
}
|> Maybe.justIf toggleable
, primaryEditor =
\_ -> ConfigOption.maybeEditor (ConfigOption.intEditor (Strings.TimeLimit { stage = stage |> Round.stageDescription }) [ WlA.min 0, WlA.max 900 ])
\_ -> ConfigOption.maybeEditor (ConfigOption.intEditor (Strings.TimeLimit { stage = stage |> Round.stageDescription }) (ConfigOption.toMinMaxAttrs timeLimitBounds))
, extraEditor = ConfigOption.noExtraEditor
, set = ConfigOption.wrappedSetter (TimeLimitChange stage)
, messages = \_ -> [ Message.info description ]
@@ -1,11 +1,10 @@
module MassiveDecks.Pages.Lobby.Invite exposing (button, dialog, overlay)
import FontAwesome.Icon as Icon
import FontAwesome.Solid as Icon
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Json.Decode
import MassiveDecks.Components as Components
import MassiveDecks.Components.Form as Form
import MassiveDecks.Components.Form.Message as Message
import MassiveDecks.Model exposing (..)
@@ -16,20 +15,19 @@ import MassiveDecks.Pages.Start.Route as Start
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Html as Html
import MassiveDecks.Util.Html.Events as HtmlE
import MassiveDecks.Util.NeList as NeList exposing (NeList(..))
import Material.Card as Card
import Material.IconButton as IconButton
import QRCode
import Url exposing (Url)
import Weightless as Wl
{-| A button to show the dialog.
-}
button : (Msg -> msg) -> Shared -> Html msg
button wrap shared =
Components.iconButton
[ Lang.title shared Strings.Invite
, ToggleInviteDialog |> wrap |> HtmlE.onClick
]
Icon.bullhorn
IconButton.view shared Strings.Invite (Icon.bullhorn |> Icon.present |> NeList.just) (ToggleInviteDialog |> wrap |> Just)
{-| A dialog overlay that displays information on how to invite people to the game.
@@ -45,15 +43,11 @@ dialog wrap shared gameCode password open =
, HtmlA.classList [ ( "open", open ) ]
, Lobby.ToggleInviteDialog |> wrap |> HtmlE.onClick
]
[ Components.iconButton
[ HtmlA.class "close"
, Lobby.ToggleInviteDialog
|> wrap
|> onClickNoPropegation
, Strings.Close |> Lang.title shared
]
Icon.times
, Wl.card [ onClickNoPropegation (wrap NoOp) ]
[ IconButton.viewNoPropagation shared
Strings.Close
(Icon.times |> Icon.present |> NeList.just)
(Lobby.ToggleInviteDialog |> wrap |> Just)
, Card.view [ HtmlE.onClickNoPropagation (wrap NoOp) ]
[ Strings.InviteExplanation { gameCode = GameCode.toString gameCode, password = password } |> Lang.html shared
, Form.section shared
"invite-link"
@@ -65,7 +59,10 @@ dialog wrap shared gameCode password open =
, HtmlA.id "invite-link-field"
]
[]
, Components.iconButton [ "invite-link-field" |> Copy |> wrap |> HtmlE.onClick ] Icon.copy
, IconButton.view shared
Strings.Copy
(Icon.copy |> Icon.present |> NeList.just)
("invite-link-field" |> Copy |> wrap |> Just)
]
)
[ Message.info Strings.InviteLinkHelp ]
@@ -92,11 +89,6 @@ overlay shared gameCode =
{- Private -}
onClickNoPropegation : msg -> Html.Attribute msg
onClickNoPropegation msg =
HtmlE.stopPropagationOn "click" (Json.Decode.succeed ( msg, True ))
qr : String -> Html msg
qr lobbyUrl =
lobbyUrl
@@ -1,6 +1,7 @@
module MassiveDecks.Pages.Lobby.Messages exposing (Msg(..))
import MassiveDecks.Animated as Animated
import MassiveDecks.Components.Menu.Model as Menu
import MassiveDecks.Game.Messages as Game
import MassiveDecks.Game.Time as Time
import MassiveDecks.Models.MdError exposing (MdError)
@@ -29,5 +30,7 @@ type Msg
| TryCast Auth
| Copy String
| ChangeSection (Maybe Section)
| SetGameMenuState Menu.State
| SetUserMenuState User.Id Menu.State
| EndGame
| NoOp
@@ -13,10 +13,11 @@ module MassiveDecks.Pages.Lobby.Model exposing
import Dict exposing (Dict)
import MassiveDecks.Animated exposing (Animated)
import MassiveDecks.Components.Menu.Model as Menu
import MassiveDecks.Error.Model exposing (Error)
import MassiveDecks.Game.Model as Game exposing (Game)
import MassiveDecks.Game.Time as Time
import MassiveDecks.Models.MdError as MdError exposing (GameStateError, MdError)
import MassiveDecks.Models.MdError exposing (GameStateError, MdError)
import MassiveDecks.Pages.Lobby.Configure.Model as Configure
import MassiveDecks.Pages.Lobby.GameCode exposing (GameCode)
import MassiveDecks.Pages.Lobby.Route exposing (..)
@@ -29,7 +30,7 @@ to somewhere else with some payload.
-}
type Change
= Stay Model
| AuthError GameCode MdError.AuthenticationError
| JoinError GameCode MdError
| LeftGame GameCode User.LeaveReason
| ConfigError Error
@@ -46,6 +47,8 @@ type alias Model =
, inviteDialogOpen : Bool
, timeAnchor : Maybe Time.Anchor
, spectate : Spectate.Model
, gameMenu : Menu.State
, userMenu : Maybe User.Id
}
@@ -10,8 +10,6 @@ import FontAwesome.Icon as Icon
import FontAwesome.Solid as Icon
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import MassiveDecks.Components as Component
import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Pages.Lobby.Actions as Actions
import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode)
@@ -27,6 +25,8 @@ import MassiveDecks.Pages.Start.Route as Start
import MassiveDecks.Strings as Strings
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.User as User
import MassiveDecks.Util.NeList as NeList
import Material.IconButton as IconButton
import QRCode
import Url exposing (Url)
@@ -96,21 +96,18 @@ viewSettings wrap changePage shared lobby =
( wrap BecomePlayer, Strings.BecomePlayerDescription )
in
[ Html.div [ HtmlA.id "spectate-actions" ]
[ Component.iconButton
[ backAction |> HtmlE.onClick
, backDescription |> Lang.title shared
]
Icon.arrowLeft
, Component.iconButton
[ { route | section = Just Lobby.Configure } |> Route.Lobby |> changePage |> HtmlE.onClick
, Strings.ViewConfgiurationDescription |> Lang.title shared
]
Icon.cog
, Component.iconButton
[ ToggleAdvert |> wrap |> HtmlE.onClick
, Strings.ToggleAdvertDescription |> Lang.title shared
]
advertiseIcon
[ IconButton.view shared
backDescription
(Icon.arrowLeft |> Icon.present |> NeList.just)
(Just backAction)
, IconButton.view shared
Strings.ViewConfgiurationDescription
(Icon.cog |> Icon.present |> NeList.just)
({ route | section = Just Lobby.Configure } |> Route.Lobby |> changePage |> Just)
, IconButton.view shared
Strings.ToggleAdvertDescription
(advertiseIcon |> Icon.present |> NeList.just)
(ToggleAdvert |> wrap |> Just)
]
]
+85 -83
View File
@@ -1,7 +1,7 @@
module MassiveDecks.Pages.Start exposing
( changeRoute
, init
, initWithAuthError
, initWithError
, route
, update
, view
@@ -21,7 +21,6 @@ import MassiveDecks.Card.Model as Card
import MassiveDecks.Card.Parts as Parts
import MassiveDecks.Card.Response as Response
import MassiveDecks.Card.Source.Model as Source
import MassiveDecks.Components as Components
import MassiveDecks.Components.Form as Form
import MassiveDecks.Components.Form.Message as Message exposing (Message)
import MassiveDecks.Error as Error
@@ -48,10 +47,14 @@ import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Html as Html
import MassiveDecks.Util.Maybe as Maybe
import MassiveDecks.Util.NeList as NeList exposing (NeList(..))
import MassiveDecks.Version as Version
import Material.Button as Button
import Material.Card as Card
import Material.IconButton as IconButton
import Material.Tabs as Tabs
import Material.TextField as TextField
import Svg.Attributes as SvgA
import Weightless as Wl
import Weightless.Attributes as WlA
changeRoute : Route -> Model -> ( Model, Cmd Global.Msg )
@@ -93,8 +96,8 @@ init shared r =
)
initWithAuthError : Shared -> GameCode -> MdError.AuthenticationError -> ( Model, Cmd Global.Msg )
initWithAuthError shared gameCode authenticationError =
initWithError : Shared -> GameCode -> MdError -> ( Model, Cmd Global.Msg )
initWithError shared gameCode error =
let
( model, cmd ) =
init shared { section = Join (Just gameCode) }
@@ -102,7 +105,7 @@ initWithAuthError shared gameCode authenticationError =
jlr =
model.joinLobbyRequest
in
( { model | joinLobbyRequest = { jlr | error = authenticationError |> MdError.Authentication |> Just } }, cmd )
( { model | joinLobbyRequest = { jlr | error = error |> Just } }, cmd )
route : Model -> Route
@@ -191,6 +194,10 @@ update shared msg model =
view : Shared -> Model -> List (Html Global.Msg)
view shared model =
let
r =
model.route
in
[ Html.div [ HtmlA.class "page start" ]
[ overlay shared model.overlay
, Html.header [ HtmlA.class "title-card" ]
@@ -201,13 +208,14 @@ view shared model =
]
]
]
, Wl.card []
[ Wl.tabGroup [ WlA.align WlA.Center ]
[ tab shared New model Icon.plus Strings.NewGame
, tab shared (Join model.gameCode) model Icon.signInAlt Strings.JoinPrivateGame
, tab shared Find model Icon.search Strings.FindPublicGame
, tab shared About model Icon.questionCircle Strings.AboutTheGame
]
, Card.view []
[ Tabs.view shared
{ selected = r.section
, change = \s -> Route.Start { r | section = s } |> Global.ChangePage
, ids = NeList New [ Join model.gameCode, Find, About ]
, tab = tabFor
, equals = sectionsMatch
}
, sectionContent shared model
]
, Html.footer [ HtmlA.class "version-info" ]
@@ -240,18 +248,32 @@ view shared model =
{- Private -}
tabFor : Section -> Tabs.TabModel
tabFor section =
case section of
New ->
Tabs.TabModel Strings.NewGame (Just Icon.plus)
Join _ ->
Tabs.TabModel Strings.JoinPrivateGame (Just Icon.signInAlt)
Find ->
Tabs.TabModel Strings.FindPublicGame (Just Icon.search)
About ->
Tabs.TabModel Strings.AboutTheGame (Just Icon.questionCircle)
overlay : Shared -> Maybe MdString -> Html Global.Msg
overlay shared content =
case content of
Just text ->
Html.div [ HtmlA.id "overlay" ]
[ Components.iconButton
[ HtmlA.class "close"
, HideOverlay |> Global.StartMsg |> HtmlE.onClick
, Strings.Close |> Lang.title shared
]
Icon.times
, Wl.card []
[ IconButton.view shared
Strings.Close
(Icon.times |> Icon.present |> NeList.just)
(HideOverlay |> Global.StartMsg |> Just)
, Card.view []
[ text |> Lang.html shared
]
]
@@ -306,24 +328,6 @@ gameCodeForRoute r =
Nothing
tab : Shared -> Section -> Model -> Icon -> MdString -> Html Global.Msg
tab shared targetSection model icon title =
let
r =
model.route
in
Wl.tab
(List.concat
[ [ Route.Start { r | section = targetSection } |> Global.ChangePage |> HtmlE.onClick
]
, [ WlA.checked ] |> Maybe.justIf (sectionsMatch r.section targetSection) |> Maybe.withDefault []
]
)
[ Icon.viewIcon icon
, title |> Lang.html shared
]
sectionsMatch : Section -> Section -> Bool
sectionsMatch first second =
case first of
@@ -381,17 +385,18 @@ newContent shared model =
|> Maybe.withDefault Html.nothing
in
Html.div [ HtmlA.class "new-game start-tab" ]
[ error
, Html.div [ HtmlA.class "restrict" ]
(List.concat
[ nameField shared model Nothing
, [ Wl.button
[ buttonAttr
]
[ buttonIcon, Strings.PlayGame |> Lang.html shared ]
]
]
)
[ Html.div [ HtmlA.class "tab-content" ]
[ Html.h2 [] [ Strings.NewGame |> Lang.html shared ]
, Html.p [] [ Html.text "Start a new game of Massive Decks." ]
, error
, nameField shared model Nothing
, Button.view shared
Button.Raised
Strings.PlayGame
Strings.PlayGame
buttonIcon
[ buttonAttr ]
]
]
@@ -437,31 +442,35 @@ joinContent shared model =
|> Maybe.withDefault []
in
Html.div [ HtmlA.class "join-game start-tab" ]
[ error
, Html.div [ HtmlA.class "restrict" ]
[ Html.div [ HtmlA.class "tab-content" ]
(List.concat
[ rejoinSection shared model
, nameField shared model nameError
, [ Form.section shared
[ [ Html.h2 [] [ Strings.JoinPrivateGame |> Lang.html shared ]
, Html.p [] [ Html.text "Join a game someone invited you to." ]
, error
]
, rejoinSection shared model
, [ nameField shared model Nothing
, Form.section shared
"game-code-input"
(Wl.textField
(TextField.view shared
Strings.GameCodeTerm
TextField.Text
(model.gameCode |> Maybe.map GameCode.toString |> Maybe.withDefault "")
[ HtmlA.class "game-code-input"
, GameCodeChanged >> Global.StartMsg |> HtmlE.onInput
, WlA.value (model.gameCode |> Maybe.map GameCode.toString |> Maybe.withDefault "")
, WlA.outlined
, Strings.GameCodeTerm |> Lang.label shared
]
[]
)
[ Message.info Strings.GameCodeHowToAcquire
, gameError |> Maybe.map Message.mdError |> Maybe.withDefault Message.none
]
]
, maybePasswordField
, [ Wl.button
[ buttonAttr
]
[ buttonIcon, Strings.PlayGame |> Lang.html shared ]
, [ Button.view shared
Button.Raised
Strings.PlayGame
Strings.PlayGame
buttonIcon
[ buttonAttr ]
]
]
)
@@ -472,13 +481,11 @@ passwordField : Shared -> Maybe MdError -> String -> List (Html Global.Msg)
passwordField shared error password =
[ Form.section shared
"password-input"
(Wl.textField
[ PasswordChanged >> Global.StartMsg |> HtmlE.onInput
, WlA.value password
, WlA.outlined
, Strings.LobbyPassword |> Lang.label shared
]
[]
(TextField.view shared
Strings.LobbyPassword
TextField.Password
password
[ PasswordChanged >> Global.StartMsg |> HtmlE.onInput ]
)
[ Message.info Strings.LobbyRequiresPassword
, error |> Maybe.map (MdError.describe >> Message.error) |> Maybe.withDefault Message.none
@@ -518,22 +525,17 @@ rejoinLobby shared result =
Nothing
nameField : Shared -> Model -> Maybe MdError -> List (Html Global.Msg)
nameField : Shared -> Model -> Maybe MdError -> Html Global.Msg
nameField shared model error =
[ Form.section shared
Form.section shared
"name-input"
(Wl.textField
[ NameChanged
>> Global.StartMsg
|> HtmlE.onInput
, WlA.value model.name
, Strings.NameLabel |> Lang.label shared
, WlA.outlined
]
[]
(TextField.view shared
Strings.NameLabel
TextField.Text
model.name
[ NameChanged >> Global.StartMsg |> HtmlE.onInput ]
)
[ error |> Maybe.map (MdError.describe >> Message.error) |> Maybe.withDefault Message.none ]
]
aboutContent : Shared -> Html Global.Msg
@@ -9,7 +9,6 @@ import FontAwesome.Icon as Icon
import FontAwesome.Solid as Icon
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Html.Keyed as HtmlK
import Http
import MassiveDecks.Messages as Global
@@ -30,8 +29,7 @@ import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Html as Html
import MassiveDecks.Util.List as List
import MassiveDecks.Util.Maybe as Maybe
import Weightless as Wl
import Weightless.Attributes as WlA
import Material.ListView as ListView
{-| Set up the lobby browser.
@@ -163,10 +161,15 @@ stateGroup shared ( state, lobbies ) =
lobby : Shared -> Summary -> ( String, Html Global.Msg )
lobby shared data =
( data.gameCode |> GameCode.toString
, Wl.listItem
[ HtmlE.onClick (Route.Start { section = Start.Join (Just data.gameCode) } |> Global.ChangePage)
, WlA.clickable
]
, ListView.viewItem
(ListView.Enabled (Route.Start { section = Start.Join (Just data.gameCode) } |> Global.ChangePage))
Nothing
([ Html.span [ HtmlA.class "lobby-game-code" ]
[ Strings.GameCode { code = GameCode.toString data.gameCode } |> Lang.html shared ]
]
|> Just
)
([ Icon.viewStyled [ HtmlA.title "Join Game" ] Icon.signInAlt ] |> Just)
[ Html.span [ HtmlA.class "lobby-name", Strings.LobbyRequiresPassword |> Lang.title shared ]
[ Html.text data.name
, Html.text " "
@@ -175,12 +178,5 @@ lobby shared data =
|> Maybe.justIf data.password
|> Maybe.withDefault Html.nothing
]
, Html.span [ HtmlA.class "lobby-game-code" ]
[ Strings.GameCode { code = GameCode.toString data.gameCode } |> Lang.html shared ]
, Icon.viewStyled
[ HtmlA.title "Join Game"
, WlA.listItemSlot WlA.AfterItem
]
Icon.signInAlt
]
)
@@ -17,7 +17,7 @@ import MassiveDecks.Pages.Unknown.Model exposing (..)
import MassiveDecks.Pages.Unknown.Route exposing (..)
import MassiveDecks.Strings exposing (MdString(..))
import MassiveDecks.Strings.Languages as Lang
import Weightless
import Material.Card as Card
changeRoute : Route -> Model -> ( Model, Cmd Msg )
@@ -38,7 +38,7 @@ route model =
view : Shared -> Model -> List (Html Msg)
view shared _ =
[ Html.div [ HtmlA.class "page unknown-page" ]
[ Weightless.card []
[ Card.view []
[ Html.h1 [] [ Icon.viewIcon Icon.exclamationCircle, Lang.html shared UnknownPageTitle ]
, Html.p []
[ Html.a [ Route.url (Route.Start { section = Start.New }) |> HtmlA.href ]
+4
View File
@@ -9,6 +9,7 @@ port module MassiveDecks.Ports exposing
, serverSend
, speechCommands
, speechVoices
, startConfetti
, storeSettings
, tryCast
)
@@ -50,3 +51,6 @@ port remoteControl : (Json.Value -> msg) -> Sub msg
port languageChanged : String -> Cmd msg
port startConfetti : String -> Cmd msg
+1 -1
View File
@@ -69,7 +69,7 @@ joinLobby msg gameCode registration =
{-| Check if previously joined lobbies are still going.
-}
checkAlive : (Request.Response Never (Dict Lobby.Token Bool) -> msg) -> List Lobby.Token -> Request msg
checkAlive : (Request.Response Never (List Lobby.Token) -> msg) -> List Lobby.Token -> Request msg
checkAlive msg tokens =
{ method = "POST"
, headers = []
@@ -13,7 +13,6 @@ import FontAwesome.Icon as Icon
import FontAwesome.Solid as Icon
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import MassiveDecks.Error as Error
import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Pages.Route exposing (Route)
@@ -21,10 +20,10 @@ import MassiveDecks.Requests.HttpData.Messages exposing (..)
import MassiveDecks.Requests.HttpData.Model exposing (..)
import MassiveDecks.Requests.Request as Request exposing (Request)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Maybe as Maybe
import MassiveDecks.Util.NeList exposing (NeList(..))
import Material.IconButton as IconButton
import Time
import Weightless as Wl
import Weightless.Attributes as WlA
{-| Set up the empty HttpData and send a request to load the data.
@@ -111,31 +110,16 @@ view shared route emptyContent viewResult model =
{-| Show a refresh button for the data.
-}
refreshButton : Shared -> HttpData error result -> Html (Msg error result)
refreshButton shared model =
refreshButton shared { loading, data } =
let
title =
[ Strings.Refresh |> Lang.title shared ]
onClick =
if not model.loading then
[ HtmlE.onClick Pull ]
applyStyle =
if loading then
Icon.styled [ Icon.spin ]
else
[]
style =
[ WlA.flat
, WlA.fab
, WlA.inverted
]
spin =
if model.loading then
[ Icon.spin ]
else
[]
identity
in
Wl.button
(List.concat [ style, title, onClick ])
[ Icon.viewStyled spin Icon.sync ]
IconButton.view shared
Strings.Refresh
(NeList (Icon.sync |> Icon.present |> applyStyle) [])
(Pull |> Maybe.justIf (not loading))
+118 -101
View File
@@ -15,13 +15,12 @@ import Dict.Extra as Dict
import FontAwesome.Attributes as Icon
import FontAwesome.Icon as Icon
import FontAwesome.Solid as Icon
import Html exposing (Html)
import Html as Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Http
import MassiveDecks.Card.Source as Source
import MassiveDecks.Card.Source.Model as Source
import MassiveDecks.Components as Components
import MassiveDecks.Components.Form as Form
import MassiveDecks.Components.Form.Message as Message
import MassiveDecks.Icon as Icon
@@ -41,11 +40,12 @@ import MassiveDecks.Speech as Speech
import MassiveDecks.Strings as Strings
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Strings.Languages.Model as Lang exposing (Language)
import MassiveDecks.Util.Html.Attributes as HtmlA
import MassiveDecks.Util.Maybe as Maybe
import Weightless as Wl
import Weightless.Attributes as WlA
import Weightless.Slider as Slider
import MassiveDecks.Util.NeList exposing (NeList(..))
import MassiveDecks.Util.Order as Order
import Material.IconButton as IconButton
import Material.Select as Select
import Material.Slider as Slider
import Material.Switch as Switch
init : (Msg -> msg) -> Settings -> ( Model, Cmd msg )
@@ -115,7 +115,7 @@ update shared msg =
let
newTokens =
model.settings.tokens
|> Dict.filter (\_ -> \t -> Dict.get t tokenValidity |> Maybe.withDefault True)
|> Dict.filter (\_ -> \t -> List.member t tokenValidity)
in
changeSettings (\s -> { s | tokens = newTokens }) model
@@ -129,9 +129,9 @@ update shared msg =
ChangeSpeech voice ->
let
newSpeechSettings =
model.settings.speech |> Speech.selectVoice voice
voice |> Maybe.map (\v -> model.settings.speech |> Speech.selectVoice v)
in
changeSettings (\s -> { s | speech = newSpeechSettings }) model
changeSettings (\s -> { s | speech = newSpeechSettings |> Maybe.withDefault s.speech }) model
ToggleAutoAdvance enabled ->
changeSettings (\s -> { s | autoAdvance = Just enabled }) model
@@ -167,15 +167,10 @@ view wrap shared =
Icon.cog
button =
Components.iconButtonStyled
[ HtmlA.id "settings-button"
, ToggleOpen |> wrap |> HtmlE.onClick
, Strings.SettingsTitle |> Lang.title shared
]
( [ Icon.lg ], icon )
IconButton.view shared Strings.SettingsTitle (NeList (icon |> Icon.present |> Icon.styled [ Icon.lg ]) []) (ToggleOpen |> wrap |> Just)
panel =
Wl.card [ HtmlA.classList [ ( "settings-panel", True ), ( "open", model.open ) ] ]
Html.div [ HtmlA.classList [ ( "settings-panel", True ), ( "mdc-card", True ), ( "open", model.open ) ] ]
[ Html.h3 [] [ Strings.SettingsTitle |> Lang.html shared ]
, Html.div [ HtmlA.class "body" ]
(List.intersperse (Html.hr [] [])
@@ -296,44 +291,30 @@ cardSize wrap shared =
"card-size"
(Html.div
[ HtmlA.class "multipart" ]
[ Wl.slider
[ HtmlA.class "primary"
, Slider.step 1
, Slider.min 1
, Slider.max 3
, Strings.CardSizeSetting |> Lang.string shared |> WlA.label
, WlA.outlined
, Slider.thumbLabel True
, String.toInt
>> Maybe.andThen cardSizeFromValue
>> Maybe.withDefault Full
>> ChangeCardSize
>> wrap
|> HtmlE.onInput
, settings.cardSize |> cardSizeToValue |> String.fromInt |> WlA.value
]
[ Icon.viewStyled [ Slider.slot Slider.Before ] Icon.searchMinus
, [ settings.cardSize |> cardSizeThumb ] |> Html.span [ Slider.slot Slider.ThumbLabel ]
, Icon.viewStyled [ Slider.slot Slider.After ] Icon.searchPlus
[ Html.div
[ HtmlA.class "card-size-slider" ]
[ Icon.viewStyled [] Icon.minimalCardSize
, Slider.view
[ HtmlA.class "primary"
, Slider.step 1
, Slider.min 1
, Slider.max 3
, Slider.pin
, Slider.markers
, cardSizeFromValue
>> Maybe.withDefault Full
>> ChangeCardSize
>> wrap
|> Slider.onChange
, settings.cardSize |> cardSizeToValue |> Slider.value
]
, Icon.viewStyled [] Icon.callCard
]
]
)
[ Message.info Strings.CardSizeExplanation ]
cardSizeThumb : CardSize -> Html msg
cardSizeThumb size =
case size of
Minimal ->
Icon.viewIcon Icon.minimalCardSize
Square ->
Icon.viewIcon Icon.squareCardSize
Full ->
Icon.viewIcon Icon.callCard
autoAdvanceRound : (Msg -> msg) -> Shared -> Html msg
autoAdvanceRound wrap shared =
let
@@ -344,11 +325,12 @@ autoAdvanceRound wrap shared =
"auto-advance"
(Html.div
[ HtmlA.class "multipart" ]
[ Wl.switch
[ Switch.view
[ HtmlE.onCheck (ToggleAutoAdvance >> wrap)
, HtmlA.checked (settings.autoAdvance |> Maybe.withDefault False)
, HtmlA.id "auto-advance-enable"
]
, Html.label []
, Html.label [ HtmlA.for "auto-advance-enable" ]
[ Icon.viewIcon Icon.commentDots
, Html.text " "
, Strings.AutoAdvanceSetting |> Lang.html shared
@@ -379,6 +361,12 @@ speechVoiceSelector wrap shared =
else
Message.none
currentLanguage =
shared |> Lang.currentLanguage
voiceSortOrder =
combinedOrder (NeList defaultFirst [ languageMatchFirst currentLanguage ])
in
Html.div []
[ Form.section shared
@@ -386,24 +374,31 @@ speechVoiceSelector wrap shared =
(Html.div []
[ Html.div
[ HtmlA.class "multipart" ]
[ Wl.switch
[ Switch.view
[ HtmlE.onCheck (ToggleSpeech >> wrap)
, HtmlA.disabled isDisabled
, HtmlA.checked enabled
, HtmlA.id "speech-enable"
]
, Html.label []
, Html.label [ HtmlA.for "speech-enable" ]
[ Icon.viewIcon Icon.commentDots
, Html.text " "
, Strings.SpeechSetting |> Lang.html shared
]
]
, Wl.select
[ HtmlE.onInput (ChangeSpeech >> wrap)
, Strings.VoiceSetting |> Lang.string shared |> WlA.label
, WlA.outlined
, HtmlA.disabled (not enabled || isDisabled)
, Html.div [ HtmlA.class "children" ]
[ Select.view shared
{ label = Strings.VoiceSetting
, idToString = identity
, idFromString = Just
, selected = selectedVoice
, wrap = ChangeSpeech >> wrap
}
[ HtmlA.disabled (not enabled || isDisabled)
, HtmlA.class "secondary"
]
(voices |> List.sortWith voiceSortOrder |> List.map speechVoiceOption)
]
(voices |> List.sortWith defaultFirst |> List.map (speechVoiceOption selectedVoice))
]
)
[ Message.info Strings.SpeechExplanation
@@ -414,7 +409,7 @@ speechVoiceSelector wrap shared =
defaultFirst : Speech.Voice -> Speech.Voice -> Order
defaultFirst a b =
if a.default && b.default then
if a.default == b.default then
EQ
else if a.default then
@@ -424,19 +419,38 @@ defaultFirst a b =
GT
speechVoiceOption : Maybe String -> Speech.Voice -> Html msg
speechVoiceOption selectedVoice voice =
let
selected =
if selectedVoice == Just voice.name then
[ WlA.selected ]
languageMatchFirst : Language -> Speech.Voice -> Speech.Voice -> Order
languageMatchFirst lang =
Lang.sortClosestFirst lang |> Order.map (.lang >> Lang.fromCode)
else
[]
in
Html.option
(HtmlA.value voice.name :: selected)
[ Html.text (voice.name ++ " (" ++ voice.lang ++ ")") ]
combinedOrder : NeList (a -> a -> Order) -> a -> a -> Order
combinedOrder (NeList first rest) a b =
case rest of
[] ->
first a b
next :: remaining ->
let
result =
first a b
in
case result of
EQ ->
combinedOrder (NeList next remaining) a b
_ ->
result
speechVoiceOption : Speech.Voice -> Select.ItemModel String msg
speechVoiceOption voice =
{ id = voice.name
, icon = Nothing
, primary = [ voice.name |> Html.text ]
, secondary = Nothing
, meta = Nothing
}
notificationsSwitch : (Msg -> msg) -> Shared -> Html msg
@@ -462,12 +476,13 @@ notificationsSwitch wrap shared =
(Html.div []
[ Html.div
[ HtmlA.class "multipart" ]
[ Wl.switch
[ Switch.view
[ HtmlE.onCheck (ToggleNotifications >> wrap)
, HtmlA.disabled unsupported
, HtmlA.checked enabled
, HtmlA.id "notifications-enable"
]
, Html.label []
, Html.label [ HtmlA.for "notifications-enable" ]
[ Icon.viewIcon Icon.bell
, Html.text " "
, Strings.NotificationsSetting |> Lang.html shared
@@ -479,12 +494,13 @@ notificationsSwitch wrap shared =
"only-when-hidden"
(Html.div
[ HtmlA.class "multipart" ]
[ Wl.switch
[ Switch.view
[ HtmlE.onCheck (ToggleOnlyWhenHidden >> wrap)
, HtmlA.disabled visibilityDisabled
, HtmlA.checked settings.requireNotVisible
, HtmlA.id "only-when-hidden-toggle"
]
, Html.label []
, Html.label [ HtmlA.for "only-when-hidden-toggle" ]
[ Icon.viewIcon Icon.eyeSlash
, Html.text " "
, Strings.NotificationOnlyWhenHiddenSetting |> Lang.html shared
@@ -520,41 +536,42 @@ languageSelector wrap shared =
Form.section
shared
"language"
(Wl.select
[ HtmlE.onInput (Lang.fromCode >> ChangeLang >> wrap)
, Strings.LanguageSetting |> Lang.string shared |> WlA.label
, WlA.outlined
]
(Lang.languages |> List.map (languageOption selected))
(Select.view shared
{ label = Strings.LanguageSetting
, idToString = Lang.code
, idFromString = Lang.fromCode
, selected = Just selected
, wrap = ChangeLang >> wrap
}
[]
(Lang.languages |> List.map (languageOption shared selected))
)
[ Message.info Strings.MissingLanguage ]
languageOption : Language -> Language -> Html msg
languageOption currentLanguage language =
languageOption : Shared -> Language -> Language -> Select.ItemModel Language msg
languageOption shared currentLanguage language =
let
autonym =
language |> Lang.autonym
nameInCurrentLanguage =
language |> Lang.languageName |> Lang.givenLanguageString currentLanguage
language
|> Lang.languageName
|> Lang.givenLanguageString currentLanguage
name =
if currentLanguage == language then
[ autonym |> Html.text ]
autonym =
if language /= currentLanguage then
language
|> Lang.autonym
|> (\n -> Just [ Strings.AutonymFormat { autonym = n } |> Lang.html shared ])
else
[ autonym |> Html.text
, Html.text " ("
, nameInCurrentLanguage |> Html.text
, Html.text ")"
]
Nothing
in
Html.option
[ WlA.selected |> Maybe.justIf (currentLanguage == language) |> Maybe.withDefault HtmlA.nothing
, language |> Lang.code |> HtmlA.value
]
name
{ id = language
, icon = Nothing
, primary = [ nameInCurrentLanguage |> Html.text ]
, secondary = autonym
, meta = Nothing
}
ignore : (Msg -> msg) -> a -> msg
@@ -1,6 +1,5 @@
module MassiveDecks.Settings.Messages exposing (Msg(..))
import Dict exposing (Dict)
import MassiveDecks.Pages.Lobby.Model as Lobby
import MassiveDecks.Settings.Model exposing (..)
import MassiveDecks.Strings.Languages.Model exposing (Language)
@@ -11,10 +10,10 @@ type Msg
| ChangeCardSize CardSize
| ChangeOpenUserList Bool
| ToggleOpen
| RemoveInvalid (Dict Lobby.Token Bool)
| RemoveInvalid (List Lobby.Token)
| ToggleSpeech Bool
| ToggleAutoAdvance Bool
| ChangeSpeech String
| ChangeSpeech (Maybe String)
| ToggleNotifications Bool
| ToggleOnlyWhenHidden Bool
| NoOp
+11
View File
@@ -38,6 +38,8 @@ type MdString
| RejoinGame { code : String } -- A description of the action of attempting to rejoin a game the user was previously in.
| LobbyRequiresPassword -- An explanation that the given lobby requires a password to join.
| YouWereKicked -- An explanation that the player was kicked from the lobby they were in.
| ScrollToTop -- A description of the action of scrolling to the top of a screen.
| Copy -- A description of the action of copying something (i.e: copy and paste).
-- Rules
| CardsAgainstHumanity -- The name of "Cards Against Humanity" (https://cardsagainsthumanity.com/).
| Rules -- The title for a DESCRIPTION of the rules.
@@ -80,6 +82,7 @@ type MdString
| SettingsTitle -- The title for the settings panel.
| LanguageSetting -- The label for the "Language" setting.
| MissingLanguage -- A question asking if the user doesn't see the language they want.
| AutonymFormat { autonym : String } -- How to format the name for a language in that language (e.g: in brackets).
| TranslationBeg -- A request for help translating the game.
| CardSizeSetting -- The label for the "Card Size" setting.
| CardSizeExplanation -- An explanation of what the card size does (changes the size of the card).
@@ -113,6 +116,10 @@ type MdString
| GameCodeSpecificDescription -- A short description of what a specific game code and how to use it.
| GameCodeHowToAcquire -- A short description of how to get a game code.
| Deck -- The name for a deck of cards.
| DeckSource -- The name for a source of decks of cards.
| DeckLanguage { language : MdString } -- A description of what language a deck is in.
| DeckAuthor { author : String } -- A description of who created the deck.
| DeckTranslator { translator : String } -- A description of who translated the deck.
| StillPlaying -- A term for a person who is in a round, but has not yet submitted a play.
| PlayingDescription -- A description of a person who is in a round, but has not yet submitted a play.
| Played -- A term for a person who is in a round, and has submitted a play.
@@ -197,6 +204,7 @@ type MdString
| WaitForDecks -- A hint that the user has to wait for decks to load before starting the game.
| MissingCardType { cardType : MdString } -- An error explaining that the user needs a deck with the given type of card (call/response).
| NotEnoughCardsOfType { cardType : MdString, needed : Int, have : Int } -- An error explaining that the user needs to add more cards of the given type for the number of players.
| AddBlankCards { amount : Int } -- A description of the action of adding the given number of blank cards to the game.
| AddDeck -- A description of the action of adding a deck to the game configuration.
| RemoveDeck -- A description of the action of removing a deck from the game configuration.
| SourceNotFound { source : MdString } -- An explanation that the deck didn't load because it doesn't exist on the source service.
@@ -246,6 +254,9 @@ type MdString
| ConflictDescription -- An explanation of what a conflict is.
| YourChanges -- A title for a section showing changes tot he configuration by the user.
| TheirChanges -- A title for a section showing changes to the configuration by someone else.
| ConfigurationDisabledWhileInGame -- A message explaining that the configuration can't be changed while the game is in-progress.
| ConfigurationDisabledIfNotPrivileged -- A message explaining that the configuration can't be changed if the user isn't privileged.
| ConfigureNextGame -- A description of the action of configuring the next game for the lobby after the current one finished.
-- Game
| SubmitPlay -- A description of the action of submitting the play for the czar to judge.
| TakeBackPlay -- A description of the action of taking back a previously submitted play.
@@ -13,6 +13,7 @@ module MassiveDecks.Strings.Languages exposing
, languages
, placeholder
, recommended
, sortClosestFirst
, string
, title
)
@@ -34,7 +35,7 @@ import MassiveDecks.Strings.Render as Render
import MassiveDecks.Strings.Translation as Translation
import MassiveDecks.Util.Maybe as Maybe
import MassiveDecks.Util.String as String
import Weightless.Attributes as WlA
import Material.Attributes as Material
{-| A list of all the languages enabled in the application, in the order they will be presented to the end-user.
@@ -75,6 +76,24 @@ autonym language =
languageName language |> givenLanguageString language
{-| A sort that gives the closest matches first.
Currently this just puts all exact matches first.
-}
sortClosestFirst : Language -> Maybe Language -> Maybe Language -> Order
sortClosestFirst target a b =
if a == b then
EQ
else if a == Just target then
LT
else if b == Just target then
GT
else
EQ
{-| The language the user is currently seeing the page in.
-}
currentLanguage : Shared -> Language
@@ -144,11 +163,11 @@ alt shared =
string shared >> String.capitalise >> HtmlA.alt
{-| Convenience for an Weightless `label` attribute from the given `MdString`.
{-| Convenience for a Material `label` attribute from the given `MdString`.
-}
label : Shared -> MdString -> Html.Attribute msg
label shared =
string shared >> String.capitalise >> WlA.label
string shared >> String.capitalise >> Material.label
{-| Get a deck to recommend to the user if they haven't added any.
@@ -15,7 +15,7 @@ pack =
{ code = "en"
, name = English
, translate = translate
, recommended = "cah-base-en" |> BuiltIn.Id |> Source.BuiltIn
, recommended = "cah-base-en" |> BuiltIn.hardcoded |> Source.BuiltIn
}
@@ -120,6 +120,12 @@ translate mdString =
YouWereKicked ->
[ Text "You were kicked from the game." ]
ScrollToTop ->
[ Text "Scroll to the top." ]
Copy ->
[ Text "Copy" ]
-- Rules
CardsAgainstHumanity ->
[ Text "Cards Against Humanity" ]
@@ -300,6 +306,9 @@ translate mdString =
MissingLanguage ->
[ Text "Dont see your language? ", Ref TranslationBeg ]
AutonymFormat { autonym } ->
[ Text "(", Text autonym, Text ")" ]
TranslationBeg ->
[ Text "Help translate "
, Ref MassiveDecks
@@ -404,6 +413,18 @@ translate mdString =
Deck ->
[ Text "Deck" ]
DeckSource ->
[ Ref Deck, Text " Source" ]
DeckLanguage { language } ->
[ Text "in ", Ref language ]
DeckAuthor { author } ->
[ Text "by ", Text author ]
DeckTranslator { translator } ->
[ Text "translation by ", Text translator ]
StillPlaying ->
[ Text "Playing" ]
@@ -622,7 +643,7 @@ translate mdString =
[ Text "End the game now." ]
ReturnViewToGame ->
[ Text "Return" ]
[ Text "Return to game" ]
ReturnViewToGameDescription ->
[ Text "Return to the main game view." ]
@@ -693,6 +714,13 @@ translate mdString =
, Text "."
]
AddBlankCards { amount } ->
[ Text "Add "
, amount |> String.fromInt |> Text
, Text " blank "
, Ref (Plural { singular = Response, amount = Just amount })
]
AddDeck ->
[ Text "Add deck." ]
@@ -864,6 +892,15 @@ translate mdString =
TheirChanges ->
[ Text "Their Changes" ]
ConfigurationDisabledWhileInGame ->
[ Text "While the game in progress, you can't change the configuration." ]
ConfigurationDisabledIfNotPrivileged ->
[ Text "You can't change the configuration of this game." ]
ConfigureNextGame ->
[ Text "Configure Next Game" ]
-- Game
SubmitPlay ->
[ Text "Give these cards to the ", Ref Czar, Text " as your play for the round." ]
@@ -13,7 +13,7 @@ pack =
{ code = "it"
, name = Italian
, translate = translate
, recommended = "cah-base-en" |> BuiltIn.Id |> Source.BuiltIn
, recommended = "cah-base-en" |> BuiltIn.hardcoded |> Source.BuiltIn
}
@@ -136,6 +136,14 @@ translate mdString =
YouWereKicked ->
[ Text "Sei stato espulso dal gioco." ]
-- TODO: Translate
ScrollToTop ->
[ Text "Scroll to the top." ]
-- TODO: Translate
Copy ->
[ Text "Copy" ]
-- Rules
CardsAgainstHumanity ->
[ Text "Cards Against Humanity" ]
@@ -316,6 +324,9 @@ translate mdString =
MissingLanguage ->
[ Text "Non vedi la tua lingua? ", Ref TranslationBeg ]
AutonymFormat { autonym } ->
[ Text "(", Text autonym, Text ")" ]
TranslationBeg ->
[ Text "Aiuta a tradurre "
, Ref MassiveDecks
@@ -420,6 +431,22 @@ translate mdString =
Deck ->
[ Text "Mazzo" ]
-- TODO: Translate
DeckSource ->
[ Ref Deck, Text " Source" ]
-- TODO: Translate
DeckLanguage { language } ->
[ Text "in ", Ref language ]
-- TODO: Translate
DeckAuthor { author } ->
[ Text "by ", Text author ]
-- TODO: Translate
DeckTranslator { translator } ->
[ Text "translation by ", Text translator ]
StillPlaying ->
[ Text "Sta giocando" ]
@@ -711,6 +738,14 @@ translate mdString =
, Text "."
]
-- TODO: Translate
AddBlankCards { amount } ->
[ Text "Add "
, amount |> String.fromInt |> Text
, Text " blank "
, Ref (Plural { singular = Response, amount = Just amount })
]
AddDeck ->
[ Text "Aggiungi mazzo." ]
@@ -883,6 +918,18 @@ translate mdString =
TheirChanges ->
[ Text "Le sue modifiche" ]
-- TODO: Translate
ConfigurationDisabledWhileInGame ->
[ Text "While the game in progress, you can't change the configuration." ]
-- TODO: Translate
ConfigurationDisabledIfNotPrivileged ->
[ Text "You can't change the configuration of this game." ]
-- TODO: Translate
ConfigureNextGame ->
[ Text "Configure Next Game" ]
-- Game
SubmitPlay ->
[ Text "Dai queste carte al ", Ref Czar, Text " come giocata per il turno." ]
@@ -11,10 +11,10 @@ import MassiveDecks.Strings.Translation as Translation exposing (Result(..))
pack : Translation.Pack
pack =
{ code = "ptBR"
{ code = "pt-BR"
, name = BrazilianPortuguese
, translate = translate
, recommended = "cah-base-ptbr" |> BuiltIn.Id |> Source.BuiltIn
, recommended = "cah-base-ptbr" |> BuiltIn.hardcoded |> Source.BuiltIn
}
@@ -137,6 +137,14 @@ translate mdString =
YouWereKicked ->
[ Text "Você foi chutado do jogo." ]
-- TODO: Translate
ScrollToTop ->
[ Text "Scroll to the top." ]
-- TODO: Translate
Copy ->
[ Text "Copy" ]
-- Rules
CardsAgainstHumanity ->
[ Text "Cartas Contra a Humanidade" ]
@@ -317,6 +325,9 @@ translate mdString =
MissingLanguage ->
[ Text "Não vê seu idioma? ", Ref TranslationBeg ]
AutonymFormat { autonym } ->
[ Text "(", Text autonym, Text ")" ]
TranslationBeg ->
[ Text "Ajudar a traduzir o "
, Ref MassiveDecks
@@ -420,6 +431,22 @@ translate mdString =
Deck ->
[ Text "Deck" ]
-- TODO: Translate
DeckSource ->
[ Ref Deck, Text " Source" ]
-- TODO: Translate
DeckLanguage { language } ->
[ Text "in ", Ref language ]
-- TODO: Translate
DeckAuthor { author } ->
[ Text "by ", Text author ]
-- TODO: Translate
DeckTranslator { translator } ->
[ Text "translation by ", Text translator ]
StillPlaying ->
[ Text "Jogando" ]
@@ -709,6 +736,14 @@ translate mdString =
, Text "."
]
-- TODO: Translate
AddBlankCards { amount } ->
[ Text "Add "
, amount |> String.fromInt |> Text
, Text " blank "
, Ref (Plural { singular = Response, amount = Just amount })
]
AddDeck ->
[ Text "Adicionar deck." ]
@@ -880,6 +915,18 @@ translate mdString =
TheirChanges ->
[ Text "Mudanças dele(a)" ]
-- TODO: Translate
ConfigurationDisabledWhileInGame ->
[ Text "While the game in progress, you can't change the configuration." ]
-- TODO: Translate
ConfigurationDisabledIfNotPrivileged ->
[ Text "You can't change the configuration of this game." ]
-- TODO: Translate
ConfigureNextGame ->
[ Text "Configure Next Game" ]
-- Game
SubmitPlay ->
[ Text "Dar essas cartas ao ", Ref Czar, Text " como sua jogada da partida." ]
@@ -1,4 +1,8 @@
module MassiveDecks.Util.Html.Attributes exposing (nothing)
module MassiveDecks.Util.Html.Attributes exposing
( fullWidth
, nothing
, slot
)
import Html exposing (Html)
import Html.Attributes as HtmlA
@@ -10,3 +14,17 @@ import Json.Encode as Json
nothing : Html.Attribute msg
nothing =
Json.null |> HtmlA.property ""
{-| The slot attribute.
-}
slot : String -> Html.Attribute msg
slot =
HtmlA.attribute "slot"
{-| The full width attribute for material components.
-}
fullWidth : Html.Attribute msg
fullWidth =
True |> Json.bool |> HtmlA.property "fullwidth"
@@ -0,0 +1,12 @@
module MassiveDecks.Util.Html.Events exposing (..)
import Html
import Html.Events as HtmlE
import Json.Decode
{-| Same as onClick, but without propagating to other elements.
-}
onClickNoPropagation : msg -> Html.Attribute msg
onClickNoPropagation msg =
HtmlE.stopPropagationOn "click" (Json.Decode.succeed ( msg, True ))
+1 -24
View File
@@ -1,7 +1,5 @@
module MassiveDecks.Util.List exposing
( any
, find
, map2Long
( map2Long
, mappedIntersperse
, merge
)
@@ -9,27 +7,6 @@ module MassiveDecks.Util.List exposing
{-| Utility functions for lists.
-}
import MassiveDecks.Util.Maybe as Maybe
{-| Returns `True` if any value in the list matches the given predicate.
-}
any : (a -> Bool) -> List a -> Bool
any predicate list =
find predicate list /= Nothing
{-| Find the first element of the given list that fulfils the given predicate.
-}
find : (a -> Bool) -> List a -> Maybe a
find predicate list =
case list of
head :: tail ->
Maybe.first [ Maybe.validate predicate head, find predicate tail ]
[] ->
Nothing
{-| Map two items together, getting `Nothing`s when the shorter ends.
Note that if longer is shorter, shorter will be cut off to match.
@@ -0,0 +1,68 @@
module MassiveDecks.Util.NeList exposing
( NeList(..)
, decoder
, extend
, fromList
, head
, just
, toList
)
{-| A module for a list that cannot be empty.
-}
import Json.Decode as Json
{-| A list that cannot be empty.
-}
type NeList item
= NeList item (List item)
{-| A list of one item.
-}
just : item -> NeList item
just item =
NeList item []
{-| Add all the new elements to the end of the list.
-}
extend : List item -> NeList item -> NeList item
extend new (NeList first rest) =
NeList first (rest ++ new)
{-| Get the first element of a non-empty list.
-}
head : NeList item -> item
head (NeList first _) =
first
{-| Convert to a normal list.
-}
toList : NeList item -> List item
toList (NeList first rest) =
first :: rest
{-| Convert from a normal list.
-}
fromList : List item -> Maybe (NeList item)
fromList items =
case items of
first :: rest ->
NeList first rest |> Just
[] ->
Nothing
{-| A JSON decoder that fails if there are no values.
-}
decoder : Json.Decoder item -> Json.Decoder (NeList item)
decoder itemDecoder =
Json.list itemDecoder
|> Json.andThen (fromList >> Maybe.map Json.succeed >> Maybe.withDefault (Json.fail "Can't be empty."))
@@ -0,0 +1,48 @@
module MassiveDecks.Util.Order exposing (..)
{-| Utilities for dealing with ordering.
-}
{-| Map an ordering.
-}
map : (a -> b) -> (b -> b -> Order) -> a -> a -> Order
map f order a b =
order (f a) (f b)
{-| Map an ordering where the value might not exist.
-}
filterMap : (b -> Order) -> (a -> Maybe b) -> (b -> b -> Order) -> a -> a -> Order
filterMap againstNothing f order a b =
case f a of
Just x ->
case f b of
Just y ->
order x y
Nothing ->
againstNothing x
Nothing ->
case f b of
Just y ->
againstNothing y |> reverse
Nothing ->
EQ
{-| Get the reversed order.
-}
reverse : Order -> Order
reverse o =
case o of
LT ->
GT
EQ ->
EQ
GT ->
LT
+1 -1
View File
@@ -8,4 +8,4 @@ module MassiveDecks.Version exposing (version)
-}
version : String
version =
"dev"
"unknown-dev"
+3 -2
View File
@@ -1,7 +1,7 @@
import { Say, Voice } from "../../ts/speech";
import {
State as NotificationState,
Command as NotificationCommand
Command as NotificationCommand,
} from "../../ts/notification-manager";
type Token = string;
@@ -61,7 +61,8 @@ export namespace Elm {
serverSend: InboundPort<ConnectionCommand>;
copyText: InboundPort<string>;
remoteControl: OutboundPort<RemoteControlCommand>;
languageChanged: InboundPort<string>
languageChanged: InboundPort<string>;
startConfetti: InboundPort<string>;
};
}
export function init(options: {
+24
View File
@@ -0,0 +1,24 @@
module Material.Attributes exposing
( label
, maxLength
, minLength
)
import Html
import Html.Attributes as HtmlA
import Json.Encode as Json
label : String -> Html.Attribute msg
label =
HtmlA.attribute "label"
minLength : Int -> Html.Attribute msg
minLength =
Json.int >> HtmlA.property "minLength"
maxLength : Int -> Html.Attribute msg
maxLength =
Json.int >> HtmlA.property "maxLength"
+47
View File
@@ -0,0 +1,47 @@
module Material.Button exposing
( Type(..)
, view
)
import Html exposing (Html)
import Html.Attributes as HtmlA
import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Html.Attributes as HtmlA
type Type
= Standard
| Outlined
| Raised
| Unelevated
view : Shared -> Type -> MdString -> MdString -> Html msg -> List (Html.Attribute msg) -> Html msg
view shared type_ label title icon attributes =
let
typeAttr =
case type_ of
Standard ->
[]
Outlined ->
[ HtmlA.attribute "outlined" "" ]
Raised ->
[ HtmlA.attribute "raised" "" ]
Unelevated ->
[ HtmlA.attribute "unelevated" "" ]
allAttrs =
List.concat
[ [ label |> Lang.label shared
, title |> Lang.title shared
]
, typeAttr
, attributes
]
in
Html.node "mwc-button" allAttrs [ Html.span [ HtmlA.slot "icon" ] [ icon ] ]
+9
View File
@@ -0,0 +1,9 @@
module Material.Card exposing (view)
import Html as HtmlA
import Html.Attributes as HtmlA
view : List (HtmlA.Attribute msg) -> List (HtmlA.Html msg) -> HtmlA.Html msg
view attributes =
HtmlA.div (HtmlA.class "mdc-card" :: attributes)
+51
View File
@@ -0,0 +1,51 @@
module Material.Fab exposing
( Type(..)
, view
)
import FontAwesome.Icon as Icon exposing (Icon)
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Html.Attributes as HtmlA
type Type
= Normal
| Extended
| Mini
view : Shared -> Type -> MdString -> Icon.Presentation id msg -> Maybe msg -> List (Html.Attribute msg) -> Html msg
view shared type_ title icon action attrs =
let
content =
case type_ of
Extended ->
[ title |> Lang.html shared ]
_ ->
[]
style =
case type_ of
Mini ->
[ HtmlA.attribute "mini" "" ]
_ ->
[]
onClick =
case action of
Just msg ->
msg |> HtmlE.onClick
Nothing ->
HtmlA.disabled True
in
Html.node "mwc-fab"
(List.concat [ [ title |> Lang.title shared, onClick ], style, attrs ])
((icon |> Icon.styled [ HtmlA.slot "icon" ] |> Icon.view) :: content)
+67
View File
@@ -0,0 +1,67 @@
module Material.IconButton exposing
( view
, viewNoPropagation
)
import FontAwesome.Icon as Icon exposing (Icon)
import FontAwesome.Layering as Icon
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Html.Events as HtmlE
import MassiveDecks.Util.NeList exposing (NeList(..))
{-| View a button that displays as a simple icon.
-}
view : Shared -> MdString -> NeList (Icon.Presentation id msg) -> Maybe msg -> Html msg
view shared title icon action =
let
actionAttr =
case action of
Just msg ->
msg |> HtmlE.onClick
Nothing ->
HtmlA.disabled True
in
viewInternal [ actionAttr ] shared title icon
{-| View a button that displays as a simple icon, and blocks clicks propagating to other elements.
-}
viewNoPropagation : Shared -> MdString -> NeList (Icon.Presentation id msg) -> Maybe msg -> Html msg
viewNoPropagation shared title icon action =
let
actionAttr =
case action of
Just msg ->
msg |> HtmlE.onClickNoPropagation
Nothing ->
HtmlA.disabled True
in
viewInternal [ actionAttr ] shared title icon
{- Private -}
viewInternal : List (Html.Attribute msg) -> Shared -> MdString -> NeList (Icon.Presentation id msg) -> Html msg
viewInternal actionAttr shared title (NeList first rest) =
let
renderedIcon =
case rest of
[] ->
first |> Icon.view
_ ->
(first :: rest) |> List.map Icon.view |> Icon.layers []
in
Html.node "mwc-icon-button"
((title |> Lang.title shared) :: actionAttr)
[ renderedIcon ]
@@ -0,0 +1,44 @@
module Material.LinearProgress exposing
( Progress(..)
, view
)
import Html exposing (Html)
import Html.Attributes as HtmlA
import Json.Encode as Json
{-| What progress to display.
-}
type
Progress
-- An unknown amount of progress.
= Indeterminate
-- A float progress from 0 (no progress) to 1 (complete).
| Progress Float
-- Complete.
| Closed
{-| A progress bar.
-}
view : Progress -> List (Html.Attribute msg) -> Html msg
view p attributes =
Html.node "mwc-linear-progress" (progress p :: attributes) []
{- Private -}
progress : Progress -> Html.Attribute msg
progress p =
case p of
Indeterminate ->
True |> Json.bool |> HtmlA.property "indeterminate"
Progress float ->
float |> Json.float |> HtmlA.property "progress"
Closed ->
True |> Json.bool |> HtmlA.property "closed"
+89
View File
@@ -0,0 +1,89 @@
module Material.ListView exposing
( Action(..)
, action
, interactive
, view
, viewItem
)
import FontAwesome.Icon as Icon exposing (Icon)
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Json.Encode as Json
import MassiveDecks.Util.Html.Attributes as HtmlA
{-| A list.
-}
view : List (Html.Attribute msg) -> List (Html msg) -> Html msg
view attributes children =
Html.node "mwc-list" attributes children
{-| What happens when you interact with the item.
-}
type Action msg
= None
| Link
| Enabled msg
| Disabled
{-| A list item that is only sometimes enabled.
-}
interactive : msg -> Bool -> Action msg
interactive msg enabled =
if enabled then
Enabled msg
else
Disabled
{-| An action from a maybe, if it is enabled.
-}
action : Maybe msg -> Action msg
action a =
case a of
Just msg ->
Enabled msg
Nothing ->
Disabled
{-| An item within a list.
-}
viewItem : Action msg -> Maybe Icon -> Maybe (List (Html msg)) -> Maybe (List (Html msg)) -> List (Html msg) -> Html msg
viewItem action_ icon secondary meta children =
let
( optionalAttrs, optionalSlots ) =
[ icon |> Maybe.map (\i -> ( HtmlA.attribute "graphic" "large", Icon.viewStyled [ HtmlA.slot "graphic" ] i ))
, meta |> Maybe.map (\m -> ( True |> Json.bool |> HtmlA.property "hasMeta", Html.span [ HtmlA.slot "meta" ] m ))
, secondary |> Maybe.map (\s -> ( True |> Json.bool |> HtmlA.property "twoline", Html.span [ HtmlA.slot "secondary" ] s ))
]
|> List.filterMap identity
|> List.unzip
actionAttr =
case action_ of
Enabled msg ->
msg |> HtmlE.onClick
Disabled ->
HtmlA.disabled True
Link ->
HtmlA.nothing
None ->
True |> Json.bool |> HtmlA.property "noninteractive"
attrs =
List.concat [ optionalAttrs, [ actionAttr ] ]
slots =
List.concat [ optionalSlots, [ Html.span [] children ] ]
in
Html.node "mwc-list-item" attrs slots
+60
View File
@@ -0,0 +1,60 @@
module Material.Menu exposing (view)
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Json.Decode
import Json.Encode as Json
import MassiveDecks.Components.Menu.Model exposing (..)
{-| View a pop-up menu.
-}
view : msg -> State -> Corner -> Html msg -> List (Html msg) -> Html msg
view onClose state corner anchor menuItems =
let
open =
case state of
Open ->
True
Closed ->
False
stringCorner =
case corner of
TopLeft ->
"TOP_LEFT"
TopRight ->
"TOP_RIGHT"
BottomLeft ->
"BOTTOM_LEFT"
BottomRight ->
"BOTTOM_RIGHT"
TopStart ->
"TOP_START"
TopEnd ->
"TOP_END"
BottomStart ->
"BOTTOM_START"
BottomEnd ->
"BOTTOM_END"
in
Html.div [ HtmlA.class "menu-anchor" ]
[ anchor
, Html.node "mwc-menu"
[ True |> Json.bool |> HtmlA.property "activatable"
, open |> Json.bool |> HtmlA.property "open"
, stringCorner |> HtmlA.attribute "corner"
, HtmlA.class "menu"
, onClose |> Json.Decode.succeed |> HtmlE.on "closed"
]
menuItems
]
+8
View File
@@ -0,0 +1,8 @@
module Material.Ripple exposing (view)
import Html as HtmlA
view : List (HtmlA.Attribute msg) -> List (HtmlA.Html msg) -> HtmlA.Html msg
view =
HtmlA.node "mwc-ripple"
+87
View File
@@ -0,0 +1,87 @@
module Material.Select exposing
( ItemModel
, Model
, view
)
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Json.Decode as Json
import Json.Encode
import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Html.Attributes as HtmlA
{-| A combo box.
-}
view : Shared -> Model id msg -> List (Html.Attribute msg) -> List (ItemModel id msg) -> Html msg
view shared model attrs items =
let
allAttrs =
List.concat
[ [ model.label |> Lang.label shared
, model.idFromString >> model.wrap |> onChange
, HtmlA.fullWidth
]
, attrs
]
in
Html.node "mwc-select" allAttrs (items |> List.map (viewItem model))
{-| The things needed to render the select.
-}
type alias Model id msg =
{ label : MdString
, idToString : id -> String
, idFromString : String -> Maybe id
, selected : Maybe id
, wrap : Maybe id -> msg
}
{-| The things needed to render a specific item in the select.
-}
type alias ItemModel id msg =
{ id : id
, icon : Maybe (Html msg)
, primary : List (Html msg)
, secondary : Maybe (List (Html msg))
, meta : Maybe (Html msg)
}
{- Private -}
{-| An item within a list.
-}
viewItem : Model id msg -> ItemModel id msg -> Html msg
viewItem { idToString, selected } { id, icon, primary, secondary, meta } =
let
( optionalAttrs, optionalSlots ) =
[ icon |> Maybe.map (\i -> ( HtmlA.attribute "graphic" "large", Html.span [ HtmlA.slot "graphic" ] [ i ] ))
, meta |> Maybe.map (\m -> ( True |> Json.Encode.bool |> HtmlA.property "hasMeta", Html.span [ HtmlA.slot "meta" ] [ m ] ))
, secondary |> Maybe.map (\s -> ( True |> Json.Encode.bool |> HtmlA.property "twoline", Html.span [ HtmlA.slot "secondary" ] s ))
]
|> List.filterMap identity
|> List.unzip
attrs =
List.concat [ optionalAttrs, [ id |> idToString |> HtmlA.value, selected == Just id |> HtmlA.selected ] ]
slots =
List.concat [ primary, optionalSlots ] |> List.intersperse (Html.text " ")
in
Html.node "mwc-list-item" attrs slots
{-| An event for when the user changes the selection.
-}
onChange : (String -> msg) -> Html.Attribute msg
onChange wrap =
Json.at [ "target", "value" ] Json.string |> Json.map wrap |> HtmlE.on "change"
+56
View File
@@ -0,0 +1,56 @@
module Material.Slider exposing
( markers
, max
, min
, onChange
, pin
, step
, value
, view
)
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Json.Decode
import Json.Encode as Json
view : List (Html.Attribute msg) -> Html msg
view attributes =
Html.node "mwc-slider" attributes []
step : Int -> Html.Attribute msg
step =
Json.int >> HtmlA.property "step"
min : Int -> Html.Attribute msg
min =
Json.int >> HtmlA.property "min"
max : Int -> Html.Attribute msg
max =
Json.int >> HtmlA.property "max"
markers : Html.Attribute msg
markers =
True |> Json.bool |> HtmlA.property "markers"
pin : Html.Attribute msg
pin =
True |> Json.bool |> HtmlA.property "pin"
value : Int -> Html.Attribute msg
value =
Json.int >> HtmlA.property "value"
onChange : (Int -> msg) -> Html.Attribute msg
onChange wrap =
Json.Decode.at [ "target", "value" ] Json.Decode.int |> Json.Decode.map wrap |> HtmlE.on "change"
+8
View File
@@ -0,0 +1,8 @@
module Material.Switch exposing (view)
import Html exposing (Html)
view : List (Html.Attribute msg) -> Html msg
view attributes =
Html.node "mwc-switch" attributes []
+88
View File
@@ -0,0 +1,88 @@
module Material.Tabs exposing
( Model
, TabModel
, view
)
import FontAwesome.Icon as Icon exposing (Icon)
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Json.Decode
import Json.Encode
import List.Extra as List
import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Html as Html
import MassiveDecks.Util.Html.Attributes as HtmlA
import MassiveDecks.Util.NeList as NonEmptyList exposing (NeList)
{-| A model for a bar of tabs.
-}
type alias Model id msg =
{ selected : id
, change : id -> msg
, ids : NeList id
, tab : id -> TabModel
, equals : id -> id -> Bool
}
{-| A model for a tab.
-}
type alias TabModel =
{ label : MdString
, icon : Maybe Icon
}
{-| View a bar of tabs.
-}
view : Shared -> Model id msg -> Html msg
view shared { selected, change, ids, tab, equals } =
let
idsList =
ids |> NonEmptyList.toList
attrs =
[ idsList |> List.findIndex (equals selected) |> Maybe.withDefault 0 |> activeIndex
, ((\id -> List.getAt id idsList) >> (ids |> NonEmptyList.head |> Maybe.withDefault) >> change) |> onActivated
]
viewTab { label, icon } =
let
( iconAttr, iconNode ) =
case icon of
Just i ->
( hasImageIcon, Icon.viewStyled [ HtmlA.slot "icon" ] i )
Nothing ->
( HtmlA.nothing, Html.nothing )
in
Html.node "mwc-tab" [ label |> Lang.label shared, iconAttr ] [ iconNode ]
tabs =
idsList |> List.map (tab >> viewTab)
in
Html.node "mwc-tab-bar" attrs tabs
{- Private -}
activeIndex : Int -> Html.Attribute msg
activeIndex =
Json.Encode.int >> HtmlA.property "activeIndex"
hasImageIcon : Html.Attribute msg
hasImageIcon =
HtmlA.attribute "hasImageIcon" "hasImageIcon"
onActivated : (Int -> msg) -> Html.Attribute msg
onActivated wrap =
Json.Decode.int |> Json.Decode.map wrap |> Json.Decode.at [ "target", "activeIndex" ] |> HtmlE.on "MDCTabBar:activated"
+8
View File
@@ -0,0 +1,8 @@
module Material.TextArea exposing (view)
import Html
view : List (Html.Attribute msg) -> List (Html.Html msg) -> Html.Html msg
view =
Html.node "mwc-textarea"
+96
View File
@@ -0,0 +1,96 @@
module Material.TextField exposing
( Type(..)
, view
)
import Html exposing (Html)
import Html.Attributes as HtmlA
import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
{-| The type of field.
-}
type Type
= Text
| Search
| Tel
| Url
| Email
| Password
| Date
| Month
| Week
| Time
| DateTimeLocal
| Number
| Color
{-| A text field for the given type of data.
-}
view : Shared -> MdString -> Type -> String -> List (Html.Attribute msg) -> Html msg
view shared label t value attributes =
let
allAttrs =
List.concat
[ [ label |> Lang.label shared
, t |> type_
, value |> HtmlA.value
]
, attributes
]
in
Html.node "mwc-textfield" allAttrs []
{- Private -}
type_ : Type -> Html.Attribute msg
type_ t =
let
stringType =
case t of
Text ->
"text"
Search ->
"search"
Tel ->
"tel"
Url ->
"url"
Email ->
"email"
Password ->
"password"
Date ->
"date"
Month ->
"month"
Week ->
"week"
Time ->
"time"
DateTimeLocal ->
"datetime-local"
Number ->
"number"
Color ->
"color"
in
stringType |> HtmlA.attribute "type"
+43
View File
@@ -0,0 +1,43 @@
module Paper.Tooltip exposing
( Position(..)
, view
)
import Html
import Html.Attributes as HtmlA
view : Position -> String -> List (Html.Html msg) -> Html.Html msg
view pos for =
Html.node "paper-tooltip" [ position pos, HtmlA.attribute "for" for ]
type Position
= Top
| Right
| Bottom
| Left
{- Private -}
position : Position -> Html.Attribute msg
position pos =
let
positionString =
case pos of
Top ->
"top"
Right ->
"right"
Bottom ->
"bottom"
Left ->
"left"
in
HtmlA.attribute "position" positionString
-95
View File
@@ -1,95 +0,0 @@
module Weightless exposing
( button
, card
, expansion
, listItem
, navigationBar
, popover
, popoverCard
, select
, slider
, switch
, tab
, tabGroup
, textArea
, textField
, tooltip
)
import Html exposing (Html)
import Weightless.Attributes as WlA
button : List (Html.Attribute msg) -> List (Html msg) -> Html msg
button =
Html.node "wl-button"
card : List (Html.Attribute msg) -> List (Html msg) -> Html msg
card =
Html.node "wl-card"
expansion : List (Html.Attribute msg) -> List (Html msg) -> Html msg
expansion attrs =
Html.node "wl-expansion" (WlA.icon "" :: attrs)
listItem : List (Html.Attribute msg) -> List (Html msg) -> Html msg
listItem =
Html.node "wl-list-item"
navigationBar : List (Html.Attribute msg) -> List (Html msg) -> Html msg
navigationBar =
Html.node "wl-nav"
popover : List (Html.Attribute msg) -> List (Html msg) -> Html msg
popover =
Html.node "wl-popover"
popoverCard : List (Html.Attribute msg) -> List (Html msg) -> Html msg
popoverCard =
Html.node "wl-popover-card"
select : List (Html.Attribute msg) -> List (Html msg) -> Html msg
select =
Html.node "wl-select"
slider : List (Html.Attribute msg) -> List (Html msg) -> Html msg
slider =
Html.node "wl-slider"
switch : List (Html.Attribute msg) -> Html msg
switch attrs =
Html.node "wl-switch" attrs []
tab : List (Html.Attribute msg) -> List (Html msg) -> Html msg
tab =
Html.node "wl-tab"
tabGroup : List (Html.Attribute msg) -> List (Html msg) -> Html msg
tabGroup =
Html.node "wl-tab-group"
textArea : List (Html.Attribute msg) -> List (Html msg) -> Html msg
textArea =
Html.node "wl-textarea"
textField : List (Html.Attribute msg) -> List (Html msg) -> Html msg
textField =
Html.node "wl-textfield"
tooltip : List (Html.Attribute msg) -> List (Html msg) -> Html msg
tooltip =
Html.node "wl-tooltip"
-432
View File
@@ -1,432 +0,0 @@
module Weightless.Attributes exposing
( Alignment(..)
, ExpansionSlot(..)
, InputType(..)
, ListItemSlot(..)
, NavSlot(..)
, TextFieldSlot(..)
, XOrigin(..)
, YOrigin(..)
, active
, align
, anchor
, anchorCloseEvents
, anchorOpenEvents
, anchorOrigin
, checked
, clickable
, disableFocusTrap
, disabled
, expansionSlot
, fab
, filled
, fixed
, flat
, hoverable
, icon
, inverted
, label
, list
, listItemSlot
, max
, maxLength
, min
, minLength
, name
, navSlot
, noRipple
, outlined
, readonly
, selected
, step
, textFieldSlot
, transformOrigin
, type_
, value
, vertical
)
import Html exposing (Html)
import Html.Attributes as HtmlA
import Json.Encode as Json
step : Int -> Html.Attribute msg
step =
numberAttr "step"
min : Int -> Html.Attribute msg
min =
numberAttr "min"
max : Int -> Html.Attribute msg
max =
numberAttr "max"
maxLength : Int -> Html.Attribute msg
maxLength =
numberAttr "maxLength"
minLength : Int -> Html.Attribute msg
minLength =
numberAttr "minLength"
anchorOpenEvents : List String -> Html.Attribute msg
anchorOpenEvents =
stringArrayProperty "anchorOpenEvents"
anchorCloseEvents : List String -> Html.Attribute msg
anchorCloseEvents =
stringArrayProperty "anchorCloseEvents"
anchor : String -> Html.Attribute msg
anchor id =
stringAttr "anchor" ("#" ++ id)
icon : String -> Html.Attribute msg
icon icon_name =
stringAttr "icon" icon_name
name : String -> Html.Attribute msg
name group =
stringAttr "name" group
type NavSlot
= Title
| Left
| Right
navSlot : NavSlot -> Html.Attribute msg
navSlot slot =
let
textSlot =
case slot of
Title ->
"title"
Left ->
"left"
Right ->
"right"
in
stringAttr "slot" textSlot
type ExpansionSlot
= ETitle
| EDescription
noRipple : Bool -> Html.Attribute msg
noRipple enabled =
HtmlA.property "noRipple" (Json.bool enabled)
expansionSlot : ExpansionSlot -> Html.Attribute msg
expansionSlot slot =
let
textSlot =
case slot of
ETitle ->
"title"
EDescription ->
"description"
in
stringAttr "slot" textSlot
type ListItemSlot
= BeforeItem
| AfterItem
listItemSlot : ListItemSlot -> Html.Attribute msg
listItemSlot slot =
let
textSlot =
case slot of
BeforeItem ->
"before"
AfterItem ->
"after"
in
stringAttr "slot" textSlot
type TextFieldSlot
= BeforeText
| AfterText
textFieldSlot : TextFieldSlot -> Html.Attribute msg
textFieldSlot slot =
let
textSlot =
case slot of
BeforeText ->
"before"
AfterText ->
"after"
in
stringAttr "slot" textSlot
value : String -> Html.Attribute msg
value text =
stringAttr "value" text
type Alignment
= Start
| Center
| End
align : Alignment -> Html.Attribute msg
align alignment =
let
textAlignment =
case alignment of
Start ->
"start"
Center ->
"center"
End ->
"end"
in
stringAttr "align" textAlignment
list : String -> Html.Attribute msg
list datalistSelector =
stringAttr "list" datalistSelector
type InputType
= Password
| Email
| Number
| Color
| Date
| Search
| Tel
| File
type_ : InputType -> Html.Attribute msg
type_ inputType =
let
textInputType =
case inputType of
Password ->
"password"
Email ->
"email"
Number ->
"number"
Color ->
"color"
Date ->
"date"
Search ->
"search"
Tel ->
"tel"
File ->
"file"
in
stringAttr "type" textInputType
label : String -> Html.Attribute msg
label text =
stringAttr "label" text
inverted : Html.Attribute msg
inverted =
presentAttribute "inverted"
vertical : Html.Attribute msg
vertical =
presentAttribute "vertical"
filled : Html.Attribute msg
filled =
presentAttribute "filled"
outlined : Html.Attribute msg
outlined =
presentAttribute "outlined"
flat : Html.Attribute msg
flat =
presentAttribute "flat"
fab : Html.Attribute msg
fab =
presentAttribute "fab"
hoverable : Html.Attribute msg
hoverable =
presentAttribute "hoverable"
disabled : Html.Attribute msg
disabled =
presentAttribute "disabled"
readonly : Html.Attribute msg
readonly =
presentAttribute "readonly"
active : Html.Attribute msg
active =
presentAttribute "active"
clickable : Html.Attribute msg
clickable =
presentAttribute "clickable"
checked : Html.Attribute msg
checked =
presentAttribute "checked"
fixed : Html.Attribute msg
fixed =
presentAttribute "fixed"
selected : Html.Attribute msg
selected =
presentAttribute "selected"
disableFocusTrap : Html.Attribute msg
disableFocusTrap =
presentAttribute "disablefocustrap"
type XOrigin
= XLeft
| XCenter
| XRight
type YOrigin
= YTop
| YCenter
| YBottom
anchorOrigin : XOrigin -> YOrigin -> List (Html.Attribute msg)
anchorOrigin =
origin "anchor"
transformOrigin : XOrigin -> YOrigin -> List (Html.Attribute msg)
transformOrigin =
origin "transform"
{- Private -}
origin : String -> XOrigin -> YOrigin -> List (Html.Attribute msg)
origin prefix xOrigin yOrigin =
let
( textXOrigin, textYOrigin ) =
textOrigin xOrigin yOrigin
in
[ stringAttr (prefix ++ "originx") textXOrigin
, stringAttr (prefix ++ "originy") textYOrigin
]
textOrigin : XOrigin -> YOrigin -> ( String, String )
textOrigin xOrigin yOrigin =
let
textXOrigin =
case xOrigin of
XLeft ->
"left"
XCenter ->
"center"
XRight ->
"right"
textYOrigin =
case yOrigin of
YTop ->
"top"
YCenter ->
"center"
YBottom ->
"bottom"
in
( textXOrigin, textYOrigin )
presentAttribute : String -> Html.Attribute msg
presentAttribute attribute =
HtmlA.attribute attribute attribute
stringArrayProperty : String -> List String -> Html.Attribute msg
stringArrayProperty property values =
HtmlA.property property (values |> Json.list Json.string)
stringAttr : String -> String -> Html.Attribute msg
stringAttr attribute stringValue =
HtmlA.attribute attribute stringValue
numberAttr : String -> Int -> Html.Attribute msg
numberAttr attribute intValue =
intValue |> String.fromInt |> HtmlA.attribute attribute
numberProp : String -> Int -> Html.Attribute msg
numberProp property intValue =
intValue |> Json.int |> HtmlA.property property
-54
View File
@@ -1,54 +0,0 @@
module Weightless.Internal exposing
( boolProp
, floatProp
, numberAttr
, numberProp
, presentAttribute
, stringArrayProperty
, stringAttr
, stringProp
)
import Html
import Html.Attributes as HtmlA
import Json.Encode as Json
presentAttribute : String -> Html.Attribute msg
presentAttribute attribute =
HtmlA.attribute attribute attribute
stringArrayProperty : String -> List String -> Html.Attribute msg
stringArrayProperty property values =
HtmlA.property property (values |> Json.list Json.string)
stringAttr : String -> String -> Html.Attribute msg
stringAttr attribute stringValue =
HtmlA.attribute attribute stringValue
numberAttr : String -> Int -> Html.Attribute msg
numberAttr attribute intValue =
intValue |> String.fromInt |> HtmlA.attribute attribute
numberProp : String -> Int -> Html.Attribute msg
numberProp property intValue =
intValue |> Json.int |> HtmlA.property property
floatProp : String -> Float -> Html.Attribute msg
floatProp property floatValue =
floatValue |> Json.float |> HtmlA.property property
boolProp : String -> Bool -> Html.Attribute msg
boolProp property boolValue =
boolValue |> Json.bool |> HtmlA.property property
stringProp : String -> String -> Html.Attribute msg
stringProp property stringValue =
stringValue |> Json.string |> HtmlA.property property
-51
View File
@@ -1,51 +0,0 @@
module Weightless.ProgressBar exposing
( determinate
, indeterminate
)
import Html exposing (Html)
import Weightless.Internal as Internal
determinate : Float -> List (Html.Attribute msg) -> Html msg
determinate completion attrs =
progressBar ([ mode Determinate, value completion ] ++ attrs) []
indeterminate : List (Html.Attribute msg) -> Html msg
indeterminate attrs =
progressBar attrs []
{- Private -}
type ProgressMode
= Indeterminate
| Determinate
progressBar : List (Html.Attribute msg) -> List (Html msg) -> Html msg
progressBar =
Html.node "wl-progress-bar"
mode : ProgressMode -> Html.Attribute msg
mode =
progressModeName >> Internal.stringProp "mode"
value : Float -> Html.Attribute msg
value =
Internal.floatProp "value"
progressModeName : ProgressMode -> String
progressModeName progressMode =
case progressMode of
Indeterminate ->
"indeterminate"
Determinate ->
"determinate"
-54
View File
@@ -1,54 +0,0 @@
module Weightless.Slider exposing
( Slot(..)
, max
, min
, slot
, step
, thumbLabel
)
import Html
import Weightless.Internal as Internal
type Slot
= Before
| After
| ThumbLabel
slot : Slot -> Html.Attribute msg
slot s =
let
textSlot =
case s of
Before ->
"before"
After ->
"after"
ThumbLabel ->
"thumb-label"
in
Internal.stringAttr "slot" textSlot
step : Int -> Html.Attribute msg
step =
Internal.numberProp "step"
min : Int -> Html.Attribute msg
min =
Internal.numberProp "min"
max : Int -> Html.Attribute msg
max =
Internal.numberProp "max"
thumbLabel : Bool -> Html.Attribute msg
thumbLabel =
Internal.boolProp "thumbLabel"
@@ -1,21 +1,25 @@
@import "./card/variables";
@import "./colors";
@import "./fonts";
@import "./card/sources";
@import "./card/size";
@use "_fonts";
@use "cards/_size";
@use "cards/_sources";
$call-color: #ffffff;
$call-background: #000000;
$response-color: $call-background;
$response-background: $call-color;
#game {
--card-aspect-ratio: #{$card-ratio};
@include fluid-type(20rem, 64rem, 0.5rem, 0.8rem);
--card-aspect-ratio: #{size.$card-ratio};
@include size.fluid-type(20rem, 64rem, 0.5rem, 0.8rem);
}
.game-card {
width: $card-width;
margin: $card-margin;
width: size.$card-width;
margin: size.$card-margin;
perspective: 100vw;
text-rendering: optimizeLegibility;
font-family: "Helvetica Neue", "Nimbus Sans L", sans-serif;
font-family: fonts.$card;
font-weight: bold;
p {
@@ -71,7 +75,7 @@
position: relative;
height: 0;
width: 100%;
@include pad-to-aspect-ratio(var(--card-aspect-ratio));
@include size.pad-to-aspect-ratio(var(--card-aspect-ratio));
& .side {
position: absolute;
@@ -1,3 +1,5 @@
/// Single source of truth for colours in the application.
$primary: #42a5f5;
$on-primary: #212121;
$primary-light: #80d6ff;
@@ -20,3 +22,5 @@ $background: #e2e2e2;
$secondary-background: #cccccc;
$transparent: rgba(#ffffff, 0);
$card: #ffffff;
+76
View File
@@ -0,0 +1,76 @@
@use "./colors";
.error-overlay {
z-index: 100;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparentize(colors.$secondary-background, 0.6);
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
padding: 3em;
.actions {
display: flex;
flex-direction: row;
justify-content: center;
}
.help {
margin-bottom: 0.5em;
}
.errors {
overflow-y: auto;
}
}
.error {
color: #{colors.$on-error};
background-color: #{colors.$error};
padding: 0.5em;
display: flex;
flex-direction: column;
.header {
display: flex;
align-items: center;
}
.title > span {
white-space: nowrap;
font-weight: bold;
.icon-prefix {
margin-right: 0.3em;
}
}
.description {
margin-left: 0.5em;
white-space: normal;
flex-basis: 100%;
}
.link {
display: block;
justify-content: flex-end;
white-space: nowrap;
}
.report {
margin: 0.5em;
background-color: inherit;
color: inherit;
height: 10em;
}
}
@@ -1,10 +1,14 @@
$general: "Roboto", sans-serif;
$card: "Helvetica Neue", "Nimbus Sans L", sans-serif;
$game-code: "Source Code Pro", monospace;
@font-face {
font-family: "Nimbus Sans L";
font-style: normal;
font-weight: 400;
src: local("Nimbus Sans L"), local("NimbusSansL"),
url("../../assets/fonts/nimbus-sans-l-regular.woff2") format("woff2");
font-display: fallback;
font-display: swap;
}
@font-face {
@@ -13,7 +17,7 @@
font-weight: 700;
src: local("Nimbus Sans L Bold"), local("NimbusSansL-Bold"),
url("../../assets/fonts/nimbus-sans-l-700.woff2") format("woff2");
font-display: fallback;
font-display: swap;
}
@font-face {
@@ -1,11 +1,10 @@
@import "./card/variables";
@import "./colors";
@import "~weightless/style/base/elevation";
@import "./game/judging";
@import "./game/playing";
@import "./game/plays";
@import "./game/history";
@import "./pages/lobby/variables";
@use "./_colors";
@use "./cards";
@use "cards/_size";
@use "./game/_judging";
@use "./game/_playing";
@use "./game/_plays";
@use "./game/_history";
// The main game area.
#game {
@@ -17,6 +16,8 @@
min-height: 100%;
//background: colors.$background url(../../assets/images/background.svg) repeat;
.action {
position: fixed;
bottom: 1em;
@@ -45,57 +46,67 @@
}
}
#new-game-config {
margin-right: 3em;
align-self: flex-end;
}
#win-confetti-overlay {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
height: 100%;
width: 100%;
pointer-events: none;
}
#top-content {
position: sticky;
z-index: 3;
top: 0;
left: 0;
right: 0;
--progress-bar-bg: #{$error};
font-size: 5em;
display: flex;
flex-direction: column;
align-items: stretch;
height: 0;
overflow: visible;
#timer {
flex-shrink: 0;
height: 0.2em;
}
#time-left {
margin: 1rem;
height: 4px;
--mdc-linear-progress-buffer-color: #{colors.$error};
}
.top-row {
margin-top: -0.2em;
height: 2.5em;
display: flex;
justify-content: space-between;
align-items: flex-start;
align-items: center;
}
.help {
display: flex;
flex-flow: row wrap;
align-items: center;
font-size: 2em;
> span {
margin-left: 1em;
flex-basis: 5em;
flex-grow: 1;
}
}
}
#minor-actions {
display: flex;
flex-direction: row;
margin: 1rem;
#context-help {
flex-grow: 0;
--card-padding: 0.5em;
font-size: 1.2rem;
wl-popover-card {
min-width: 10em;
max-width: 20em;
}
}
wl-button {
font-size: 1.2rem;
margin: 0;
}
margin: 0.5em 1em 0 1em;
}
// The button to scroll to the top of the game area.
@@ -110,7 +121,7 @@
top: 0;
bottom: 0;
wl-button {
mwc-icon-button {
pointer-events: all;
position: sticky;
bottom: 0.5em;
@@ -137,7 +148,7 @@
flex-direction: column;
justify-content: center;
align-items: center;
padding: 2em 0;
padding: 0;
z-index: 2;
}
@@ -146,7 +157,7 @@
ol.card-set {
&.picked {
.side {
border-color: $primary;
border-color: colors.$primary;
}
}
}
@@ -163,7 +174,7 @@ ul.cards {
margin-top: 0.5em;
li {
min-width: $card-width;
min-width: size.$card-width;
}
}
@@ -177,7 +188,7 @@ ol.card-set {
padding: 0;
li {
height: $card-overlap;
height: size.$card-overlap;
overflow: visible;
&:hover {
@@ -185,7 +196,7 @@ ol.card-set {
}
&:last-child {
height: height($card-width, var(--card-aspect-ratio));
height: size.height(size.$card-width, var(--card-aspect-ratio));
}
}
}
+93
View File
@@ -0,0 +1,93 @@
@use "_colors";
@use "_fonts";
@use "~@material/card";
@include card.core-styles;
/// Support styling for Material Web Components.
:root {
--mdc-theme-primary: #{colors.$primary};
--mdc-theme-on-primary: #{colors.$on-primary};
--mdc-theme-secondary: #{colors.$secondary};
--mdc-theme-on-secondary: #{colors.$on-secondary};
--mdc-theme-surface: #{colors.$card};
--mdc-theme-background: #{colors.$background};
--mdc-typography-font-family: #{fonts.$general};
--mdc-menu-item-height: 48px;
--mdc-icon-size: 0.75em;
--mdc-icon-button-size: 1.5em;
}
mwc-icon-button {
svg,
.fa-layers {
display: block !important;
width: 0.7em !important;
height: 0.7em !important;
}
}
.mdc-card {
padding: 1em;
}
mwc-select {
width: 100%;
}
mwc-textfield {
width: 100%;
}
mwc-fab {
line-height: 1;
&:focus,
&::-moz-focus-inner {
outline: none;
border: 0;
}
.svg-inline--fa {
margin-right: 0;
}
display: inline-flex;
justify-content: center;
transition: opacity 15ms linear 30ms,
transform 0.27s cubic-bezier(0, 0, 0.2, 1) 0ms;
&.exited {
transition: opacity 15ms linear 0.15s,
transform 0.18s cubic-bezier(0.4, 0, 1, 1) 0ms;
transform: scale(0);
opacity: 0;
}
&.large {
--button-fab-size: 3em;
font-size: 1.2em;
}
}
.menu-anchor {
position: relative;
mwc-menu {
position: absolute;
right: 0;
top: 0;
}
}
a > mwc-list-item {
height: 48px;
}
@@ -1,4 +1,4 @@
@import "colors";
@use "_colors";
.start-settings {
position: fixed;
@@ -26,6 +26,8 @@
padding: 0 1em 1em 1em;
border-radius: 0.5em;
color: colors.$on-primary;
.title {
display: flex;
align-items: center;
@@ -59,3 +61,14 @@
margin: 0;
}
}
.card-size-slider {
flex-grow: 1;
display: flex;
align-items: center;
gap: 1em;
}
.secondary {
flex-grow: 1;
}
-37
View File
@@ -1,37 +0,0 @@
$call-color: #ffffff;
$call-background: #000000;
$response-color: $call-background;
$response-background: $call-color;
$card-width: 18em;
$card-margin: 0.5em;
$card-overlap: 4em;
$card-ratio: 5/7;
$card-compact-ratio: 1;
@mixin pad-to-aspect-ratio($ratio) {
padding-top: #{calc(1 / #{$ratio} * 100%)};
}
@function height($width, $ratio) {
@return calc(#{$width} * 1 / #{$ratio});
}
@function strip-unit($value) {
@return $value / ($value * 0 + 1);
}
@mixin fluid-type($min-vw, $max-vw, $min-font-size, $max-font-size) {
font-size: $min-font-size;
@media screen and (min-width: $min-vw) {
font-size: calc(
#{$min-font-size} + #{strip-unit($max-font-size - $min-font-size)} *
((100vw - #{$min-vw}) / #{strip-unit($max-vw - $min-vw)})
);
}
@media screen and (min-width: $max-vw) {
font-size: $max-font-size;
}
}
@@ -1,4 +1,34 @@
@import "./variables";
$card-width: 18em;
$card-margin: 0.5em;
$card-overlap: 4em;
$card-ratio: 5/7;
$card-compact-ratio: 1;
@mixin pad-to-aspect-ratio($ratio) {
padding-top: #{calc(1 / #{$ratio} * 100%)};
}
@function height($width, $ratio) {
@return calc(#{$width} * 1 / #{$ratio});
}
@function strip-unit($value) {
@return $value / ($value * 0 + 1);
}
@mixin fluid-type($min-vw, $max-vw, $min-font-size, $max-font-size) {
font-size: $min-font-size;
@media screen and (min-width: $min-vw) {
font-size: calc(
#{$min-font-size} + #{strip-unit($max-font-size - $min-font-size)} *
((100vw - #{$min-vw}) / #{strip-unit($max-vw - $min-vw)})
);
}
@media screen and (min-width: $max-vw) {
font-size: $max-font-size;
}
}
.minimal-card-size #game {
--card-aspect-ratio: 0;
@@ -36,7 +66,7 @@
}
}
.info {
.card-info {
display: none;
}
@@ -4,7 +4,7 @@
height: 1em;
font-size: inherit;
vertical-align: -0.125em;
background-image: url(../../assets/images/cardcast.png);
background-image: url(../../../assets/images/cardcast.png);
background-size: contain;
margin-right: 0.5em;
}
-60
View File
@@ -1,60 +0,0 @@
@import "./colors";
.error-overlay {
z-index: 100;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparentize($secondary-background, 0.6);
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
padding: 3em;
.actions {
display: flex;
flex-direction: row;
justify-content: center;
}
.help {
margin-bottom: 0.5em;
}
.errors {
overflow-y: auto;
}
}
.error {
--expansion-color: #{$on-error};
--expansion-header-description-color: #{$on-error};
--expansion-bg: #{$error};
padding: 0.25em;
.description {
white-space: normal;
flex-basis: 100%;
}
.report {
.body {
margin: 0 1em;
--input-color: #{$on-error};
}
.link {
margin-top: 1em;
display: flex;
justify-content: flex-end;
width: 100%;
}
}
}
@@ -1,5 +1,5 @@
@import "../colors";
@import "../card/size";
@use "../_colors";
@use "../cards/_size";
#history {
h2 {
@@ -15,10 +15,10 @@
counter-reset: historic-round-number;
.historic-round:nth-child(even) {
background-color: transparentize($primary-light, 0.6);
background-color: transparentize(colors.$primary-light, 0.6);
}
.historic-round:nth-child(odd) {
background-color: transparentize($secondary-light, 0.6);
background-color: transparentize(colors.$secondary-light, 0.6);
}
}
@@ -42,8 +42,8 @@
}
.historic-call {
flex-basis: $card-width;
min-width: $card-width;
flex-basis: size.$card-width;
min-width: size.$card-width;
& > div {
margin: 0.5em;
@@ -1,4 +1,4 @@
@import "../colors";
@use "../_colors";
.liked {
.side::after {
@@ -12,14 +12,7 @@
display: block;
content: "";
opacity: 0.3;
//noinspection CssInvalidFunction
background: inline-svg(
"./assets/images/certificate-thumbs-up.svg",
(
path: (
fill: $primary
)
)
);
background: colors.$primary;
mask-image: url("../../../assets/images/certificate-thumbs-up.svg");
}
}
@@ -1,4 +1,4 @@
@import "../card/variables";
@use "../cards/_size";
// A hand of cards.
.hand {
@@ -28,7 +28,7 @@
// Unknown plays for the round animated in at the top of the screen during the playing stage.
.background-plays {
--card-aspect-ratio: #{$card-ratio};
--card-aspect-ratio: #{size.$card-ratio};
position: absolute;
top: 0;
@@ -45,10 +45,11 @@
.play {
position: relative;
transform: translateY(-$card-width * 4);
transform: translateY(size.$card-width * -4);
transition: transform 500ms ease-in;
width: $card-width + $card-margin * 2;
margin-left: -0.5 * ($card-width + $card-margin * 2);
width: 0;
display: flex;
justify-content: center;
.game-card {
transform: rotate(0);
@@ -56,9 +57,6 @@
transform-origin: center;
position: absolute;
top: 0;
right: 0;
left: 0;
}
&.played {
@@ -66,7 +64,7 @@
transition: transform 500ms ease-out;
.game-card {
transform: rotate(var(--rotation));
transform: rotate(var(--rotation, 0));
transition: transform 500ms ease-out;
}
}

Some files were not shown because too many files have changed in this diff Show More