diff --git a/tutor/database.py b/tutor/database.py index d3ecbbd..c8103d6 100644 --- a/tutor/database.py +++ b/tutor/database.py @@ -224,6 +224,7 @@ async def advanced_search( rarity=tutor.models.Rarity.from_string(row["rarity"]), color_identity=tutor.models.Color.from_string(row["color_identity"]), cmc=row["cmc"], + mana_cost=row["mana_cost"], type_line=row["type_line"], release_date=datetime.date.fromisoformat(row["release_date"]), games=set(), @@ -373,6 +374,7 @@ async def get_decks( 'name', "oracle_latest"."name", 'color_identity', "oracle_latest"."color_identity", 'cmc', "oracle_latest"."cmc", + 'mana_cost', "oracle_latest"."mana_cost", 'type_line', "oracle_latest"."type_line", 'edhrec_rank', "oracle_latest"."edhrec_rank", 'oracle_text', "oracle_latest"."oracle_text", @@ -407,6 +409,7 @@ async def get_decks( card["color_identity"] ), cmc=card["cmc"], + mana_cost=card["mana_cost"], type_line=card["type_line"], games=set(), legalities={}, @@ -441,6 +444,7 @@ async def get_deck( 'name', "oracle_latest"."name", 'color_identity', "oracle_latest"."color_identity", 'cmc', "oracle_latest"."cmc", + 'mana_cost', "oracle_latest"."mana_cost", 'type_line', "oracle_latest"."type_line", 'edhrec_rank', "oracle_latest"."edhrec_rank", 'oracle_text', "oracle_latest"."oracle_text", @@ -474,6 +478,7 @@ async def get_deck( card["color_identity"] ), cmc=card["cmc"], + mana_cost=card["mana_cost"], type_line=card["type_line"], games=set(), legalities={}, diff --git a/www/elm.json b/www/elm.json index 97f8346..12009ab 100644 --- a/www/elm.json +++ b/www/elm.json @@ -14,6 +14,7 @@ "elm/html": "1.0.0", "elm/http": "2.0.0", "elm/json": "1.1.3", + "elm/parser": "1.1.0", "elm/regex": "1.0.0", "elm/url": "1.0.0", "elm-community/maybe-extra": "5.2.0", diff --git a/www/src/App.elm b/www/src/App.elm index 3b0bc0f..8993900 100644 --- a/www/src/App.elm +++ b/www/src/App.elm @@ -6,7 +6,7 @@ import Browser.Events import Browser.Navigation import Card import Color -import Dict +import Dict exposing (Dict) import Element as E import Element.Background as Background import Element.Border as Border @@ -23,6 +23,7 @@ import Pages.DeckList import Paginated import Route import Spinner +import Symbol import Task import UI import Url @@ -37,6 +38,7 @@ type alias Model = , viewport : UI.Dimensions , device : E.Device , route : Maybe Route.Route + , symbols : Dict String Symbol.Symbol , page : Page } @@ -48,6 +50,7 @@ type Msg | CollectionMsg Pages.Collection.Msg | DeckListMsg Pages.DeckList.Msg | DeckEditorMsg Pages.DeckEditor.Msg + | GotSymbology (Result Http.Error (List Symbol.Symbol)) | SpinnerMsg Spinner.Msg @@ -70,6 +73,9 @@ init _ url key = route = Route.fromUrl url + symbols = + Dict.empty + initWith : (pageModel -> Page) -> (pageMsg -> Msg) -> ( pageModel, Cmd pageMsg ) -> ( Page, Cmd Msg ) initWith pageType pageMsg ( subModel, subCmd ) = ( pageType subModel, Cmd.map pageMsg subCmd ) @@ -78,7 +84,7 @@ init _ url key = case route of Just Route.Collection -> initWith Collection CollectionMsg <| - Pages.Collection.init key url device + Pages.Collection.init key url device symbols Just Route.DeckList -> initWith DeckList DeckListMsg <| @@ -86,7 +92,7 @@ init _ url key = Just (Route.Deck deckId) -> initWith DeckEditor DeckEditorMsg <| - Pages.DeckEditor.init key url device deckId + Pages.DeckEditor.init key url device symbols deckId _ -> ( NotFound, Cmd.none ) @@ -96,10 +102,12 @@ init _ url key = , viewport = viewport , device = device , route = route + , symbols = symbols , page = page } , Cmd.batch [ UI.getViewport ViewportChanged + , getSymbology , pageCmd ] ) @@ -128,7 +136,7 @@ update msg model = |> updateWith Collection CollectionMsg model ( _, Just Route.Collection ) -> - Pages.Collection.init model.navigationKey url model.device + Pages.Collection.init model.navigationKey url model.device model.symbols |> updateWith Collection CollectionMsg model ( _, Just Route.DeckList ) -> @@ -136,7 +144,7 @@ update msg model = |> updateWith DeckList DeckListMsg model ( _, Just (Route.Deck deckId) ) -> - Pages.DeckEditor.init model.navigationKey url model.device deckId + Pages.DeckEditor.init model.navigationKey url model.device model.symbols deckId |> updateWith DeckEditor DeckEditorMsg model _ -> @@ -160,6 +168,32 @@ update msg model = , Cmd.none ) + ( GotSymbology (Ok symbols), _ ) -> + let + table = + symbols + |> List.map (\s -> ( s.symbol, s )) + |> Dict.fromList + + ( newModel, cmd ) = + case model.page of + Collection pageModel -> + Pages.Collection.update (Pages.Collection.GotSymbols table) pageModel + |> updateWith Collection CollectionMsg model + + DeckEditor pageModel -> + Pages.DeckEditor.update (Pages.DeckEditor.GotSymbols table) pageModel + |> updateWith DeckEditor DeckEditorMsg model + + _ -> + ( model, Cmd.none ) + in + ( { newModel + | symbols = table + } + , cmd + ) + ( SpinnerMsg spinnerMsg, Collection pageModel ) -> Pages.Collection.update (Pages.Collection.SpinnerMsg spinnerMsg) pageModel |> updateWith Collection CollectionMsg model @@ -290,6 +324,17 @@ subscriptions model = Sub.batch global +getSymbology : Cmd Msg +getSymbology = + Http.get + { url = Url.Builder.crossOrigin "https://api.scryfall.com" [ "symbology" ] [] + , expect = + Http.expectJson GotSymbology <| + Json.Decode.at [ "data" ] <| + Json.Decode.list Symbol.decode + } + + main : Program Json.Decode.Value Model Msg main = Browser.application diff --git a/www/src/Card.elm b/www/src/Card.elm index be4bac5..9be40a7 100644 --- a/www/src/Card.elm +++ b/www/src/Card.elm @@ -16,6 +16,7 @@ type alias Prices = type alias Oracle = { oracleId : String , name : String + , manaCost : String , typeLine : String , oracleText : String } @@ -26,6 +27,7 @@ type alias Card = , name : String , setCode : String , rarity : String + , manaCost : String , typeLine : String , oracleText : String , prices : Prices @@ -44,6 +46,10 @@ decodeOracle = Json.Decode.succeed Oracle |> JDP.required "oracle_id" Json.Decode.string |> JDP.required "name" Json.Decode.string + |> JDP.required "mana_cost" + (Json.Decode.nullable Json.Decode.string + |> Json.Decode.map (Maybe.withDefault "") + ) |> JDP.required "type_line" Json.Decode.string |> JDP.required "oracle_text" (Json.Decode.nullable Json.Decode.string @@ -58,6 +64,10 @@ decode = |> JDP.required "name" Json.Decode.string |> JDP.required "set_code" Json.Decode.string |> JDP.required "rarity" Json.Decode.string + |> JDP.required "mana_cost" + (Json.Decode.nullable Json.Decode.string + |> Json.Decode.map (Maybe.withDefault "") + ) |> JDP.required "type_line" Json.Decode.string |> JDP.required "oracle_text" (Json.Decode.nullable Json.Decode.string diff --git a/www/src/Pages/Collection.elm b/www/src/Pages/Collection.elm index e239b0a..3033b75 100644 --- a/www/src/Pages/Collection.elm +++ b/www/src/Pages/Collection.elm @@ -20,6 +20,7 @@ import Json.Decode.Pipeline as JDP import Maybe.Extra import Paginated import Spinner +import Symbol import Task import UI import Url @@ -59,6 +60,7 @@ type alias Model = , cardPage : CardPage , activeCard : Maybe Card.Copy , collectionStatistics : Maybe Statistics + , symbols : Symbol.Table } @@ -74,6 +76,7 @@ type Msg | FoundCards (Result Http.Error (Paginated.Page Card.Copy)) | ShowCardDetails Card.Copy | ClearCardDetails + | GotSymbols Symbol.Table type CriteriaMsg @@ -179,8 +182,8 @@ criteriaFromUrl url = |> Maybe.withDefault emptyCriteria -init : Browser.Navigation.Key -> Url.Url -> E.Device -> ( Model, Cmd Msg ) -init key url device = +init : Browser.Navigation.Key -> Url.Url -> E.Device -> Symbol.Table -> ( Model, Cmd Msg ) +init key url device symbols = let criteria = criteriaFromUrl url @@ -193,6 +196,7 @@ init key url device = , cardPage = Loading Paginated.empty , activeCard = Nothing , collectionStatistics = Nothing + , symbols = symbols } , Cmd.batch [ UI.getViewport ViewportChanged @@ -287,6 +291,9 @@ update msg model = ClearCardDetails -> ( { model | activeCard = Nothing }, Cmd.none ) + GotSymbols symbols -> + ( { model | symbols = symbols }, Cmd.none ) + searchBar : Model -> E.Element Msg searchBar model = @@ -480,7 +487,7 @@ viewCardBrowser model = [] ) in - UI.cardRow { foil = copy.foil, subtitle = copy.collection } cardAttrs copy.card + UI.cardRow { foil = copy.foil, subtitle = copy.collection } cardAttrs model.symbols copy.card in E.column attrs [ E.row diff --git a/www/src/Pages/DeckEditor.elm b/www/src/Pages/DeckEditor.elm index 9199bd6..cec6424 100644 --- a/www/src/Pages/DeckEditor.elm +++ b/www/src/Pages/DeckEditor.elm @@ -12,6 +12,7 @@ import Http import Paginated import Route import Spinner +import Symbol import UI import Url import Url.Builder @@ -22,6 +23,7 @@ type alias Model = , url : Url.Url , device : E.Device , spinner : Spinner.Model + , symbols : Symbol.Table , deck : Deck } @@ -30,6 +32,7 @@ type Msg = ViewportChanged UI.Dimensions | SpinnerMsg Spinner.Msg | GotDeck (Result Http.Error Deck.Deck) + | GotSymbols Symbol.Table type Deck @@ -85,12 +88,13 @@ groups cards = ] -init : Browser.Navigation.Key -> Url.Url -> E.Device -> Int -> ( Model, Cmd Msg ) -init key url device deckId = +init : Browser.Navigation.Key -> Url.Url -> E.Device -> Symbol.Table -> Int -> ( Model, Cmd Msg ) +init key url device symbols deckId = ( { navigationKey = key , url = url , device = device , spinner = Spinner.init + , symbols = symbols , deck = Loading } , getDeck deckId @@ -116,9 +120,12 @@ update msg model = GotDeck (Err _) -> ( { model | deck = Failed }, Cmd.none ) + GotSymbols symbols -> + ( { model | symbols = symbols }, Cmd.none ) -viewDeck : Deck.Deck -> E.Element Msg -viewDeck deck = + +viewDeck : Symbol.Table -> Deck.Deck -> E.Element Msg +viewDeck symbols deck = let viewGroup group = E.column [ E.spacing 10 ] @@ -131,6 +138,7 @@ viewDeck deck = , subtitle = "x" ++ String.fromInt dc.quantity } [ E.width <| E.px 400, E.clipX, Background.color UI.colors.background ] + symbols dc.card ) group.cards @@ -159,7 +167,7 @@ view model = ] [ case model.deck of Ready deck -> - viewDeck deck + viewDeck model.symbols deck Failed -> E.none diff --git a/www/src/Symbol.elm b/www/src/Symbol.elm new file mode 100644 index 0000000..1e8f94f --- /dev/null +++ b/www/src/Symbol.elm @@ -0,0 +1,109 @@ +module Symbol exposing (Symbol, Table, Text, decode, text) + +import Dict exposing (Dict) +import Element as E +import Element.Background as Background +import Element.Border as Border +import Element.Font as Font +import Json.Decode +import Json.Decode.Pipeline as JDP +import Parser as P exposing ((|.), (|=)) + + +type alias Symbol = + { symbol : String + , svgURI : String + , english : String + } + + +type alias Table = + Dict String Symbol + + +type Text + = Text String + | Inline Symbol + + +decode : Json.Decode.Decoder Symbol +decode = + Json.Decode.succeed Symbol + |> JDP.required "symbol" Json.Decode.string + |> JDP.required "svg_uri" Json.Decode.string + |> JDP.required "english" Json.Decode.string + + +symbolTextParser : Dict String Symbol -> P.Parser (List Text) +symbolTextParser available = + let + lookupSymbol : String -> Text + lookupSymbol string = + Dict.get string available + |> Maybe.map Inline + |> Maybe.withDefault (Text "") + + textParser : P.Parser Text + textParser = + P.chompUntilEndOr "{" + |> P.getChompedString + |> P.map Text + + symbolStringParser : P.Parser String + symbolStringParser = + P.getChompedString <| + P.succeed () + |. P.symbol "{" + |. P.chompWhile (\c -> c /= '}') + |. P.symbol "}" + + symbolParser : P.Parser Text + symbolParser = + symbolStringParser + |> P.map lookupSymbol + in + P.loop [] + (\reversedParts -> + P.oneOf + [ P.end + |> P.map (\_ -> P.Done (List.reverse reversedParts)) + , symbolParser + |> P.map (\part -> P.Loop (part :: reversedParts)) + , textParser + |> P.map (\part -> P.Loop (part :: reversedParts)) + ] + ) + + +toElement : Int -> Text -> E.Element msg +toElement size symbolText = + let + padding = + size // 6 + + fontSize = + size - padding + + symbol : Symbol -> E.Element msg + symbol s = + E.image + [ E.width <| E.px size + , E.height <| E.px size + ] + { src = s.svgURI + , description = s.english + } + in + case symbolText of + Text s -> + E.el [ Font.size size, E.height <| E.px size ] <| E.text s + + Inline s -> + symbol s + + +text : Dict String Symbol -> Int -> String -> E.Element msg +text available size string = + E.row [ E.spacing (size // 10) ] <| + List.map (toElement size) <| + (P.run (symbolTextParser available) string |> Result.withDefault []) diff --git a/www/src/UI.elm b/www/src/UI.elm index 2f20055..2de5907 100644 --- a/www/src/UI.elm +++ b/www/src/UI.elm @@ -21,6 +21,7 @@ import Element.Border as Border import Element.Font as Font import Maybe.Extra import Spinner +import Symbol import Task import Url.Builder @@ -86,6 +87,32 @@ colors = , rare = rare , uncommon = uncommon , common = common + , mana = + { white = + { fg = E.rgb255 249 250 244 + , bg = E.rgb255 248 231 185 + } + , black = + { fg = E.rgb255 21 11 0 + , bg = E.rgb255 166 159 157 + } + , blue = + { fg = E.rgb255 14 104 171 + , bg = E.rgb255 179 206 234 + } + , green = + { fg = E.rgb255 0 114 62 + , bg = E.rgb255 196 211 202 + } + , red = + { fg = E.rgb255 211 32 42 + , bg = E.rgb255 235 159 130 + } + , generic = + { fg = E.rgb255 190 190 190 + , bg = E.rgb255 250 250 250 + } + } } @@ -173,8 +200,8 @@ priceBadge { currency, amount } = ] -cardRow : { foil : Bool, subtitle : String } -> List (E.Attribute msg) -> Card.Card -> E.Element msg -cardRow options attributes card = +cardRow : { foil : Bool, subtitle : String } -> List (E.Attribute msg) -> Symbol.Table -> Card.Card -> E.Element msg +cardRow options attributes symbols card = let badge color foil string = E.el @@ -265,7 +292,8 @@ cardRow options attributes card = , description = card.name } , E.column [ E.centerY, E.height E.fill, E.width E.fill, E.clipX ] - [ E.el [ Font.color colors.title ] <| E.text card.name + [ Symbol.text symbols 12 card.manaCost + , E.el [ Font.color colors.title ] <| E.text card.name , E.el [ Font.size 16, Font.italic, Font.color colors.subtitle ] <| E.text options.subtitle ] , E.column [ E.alignRight, E.height E.fill ] <|