1
0

10 Commity 9013f992e7 ... 0ea68be7bd

Autor SHA1 Správa Dátum
  Cadel Watson 0ea68be7bd Style set selection 1 rok pred
  Cadel Watson 1b132327b3 Test with OKR draft 1 rok pred
  Cadel Watson 13a116bfa9 Start draft from set 1 rok pred
  Cadel Watson afe3fbb2db Load initial sets from local storage 1 rok pred
  Cadel Watson 313bbe98a9 Save set data to indexeddb 1 rok pred
  Cadel Watson 6073ce4c02 Encode data ready for indexeddb 1 rok pred
  Cadel Watson 6a97056901 Fetch and save set data from server 1 rok pred
  Cadel Watson b0357858c8 Trigger fetch 1 rok pred
  Cadel Watson 37d1ce9e01 Try to read set data from IndexedDB 1 rok pred
  Cadel Watson 4fd777ea5a Load initial sets 1 rok pred
10 zmenil súbory, kde vykonal 730 pridanie a 87 odobranie
  1. 0 0
      data/draft_okr_bad.json
  2. 39 37
      elm.json
  3. 79 11
      js/app.js
  4. 34 0
      server/main.py
  5. 59 0
      src/API.elm
  6. 1 0
      src/Card.elm
  7. 27 0
      src/Components/Button.elm
  8. 149 12
      src/Database.elm
  9. 220 27
      src/Main.elm
  10. 122 0
      tests/TestDatabase.elm

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
data/draft_okr_bad.json


+ 39 - 37
elm.json

@@ -1,41 +1,43 @@
 {
-    "type": "application",
-    "source-directories": [
-        "src"
-    ],
-    "elm-version": "0.19.1",
-    "dependencies": {
-        "direct": {
-            "NoRedInk/elm-json-decode-pipeline": "1.0.1",
-            "elm/browser": "1.0.2",
-            "elm/core": "1.0.5",
-            "elm/html": "1.0.0",
-            "elm/json": "1.1.3",
-            "elm/parser": "1.1.0",
-            "elm/svg": "1.0.1",
-            "elm-community/list-extra": "8.7.0",
-            "myrho/elm-round": "1.0.5",
-            "terezka/elm-charts": "4.0.0"
-        },
-        "indirect": {
-            "K-Adam/elm-dom": "1.0.0",
-            "danhandrea/elm-time-extra": "1.1.0",
-            "elm/time": "1.0.0",
-            "elm/url": "1.0.0",
-            "elm/virtual-dom": "1.0.2",
-            "justinmimbs/date": "4.1.0",
-            "justinmimbs/time-extra": "1.2.0",
-            "ryan-haskell/date-format": "1.0.0",
-            "terezka/intervals": "2.0.2"
-        }
+  "type": "application",
+  "source-directories": [
+    "src"
+  ],
+  "elm-version": "0.19.1",
+  "dependencies": {
+    "direct": {
+      "NoRedInk/elm-json-decode-pipeline": "1.0.1",
+      "elm/browser": "1.0.2",
+      "elm/core": "1.0.5",
+      "elm/html": "1.0.0",
+      "elm/http": "2.0.0",
+      "elm/json": "1.1.3",
+      "elm/parser": "1.1.0",
+      "elm/svg": "1.0.1",
+      "elm-community/list-extra": "8.7.0",
+      "myrho/elm-round": "1.0.5",
+      "terezka/elm-charts": "4.0.0"
     },
-    "test-dependencies": {
-        "direct": {
-            "elm-explorations/test": "2.2.0"
-        },
-        "indirect": {
-            "elm/bytes": "1.0.8",
-            "elm/random": "1.0.0"
-        }
+    "indirect": {
+      "K-Adam/elm-dom": "1.0.0",
+      "danhandrea/elm-time-extra": "1.1.0",
+      "elm/bytes": "1.0.8",
+      "elm/file": "1.0.5",
+      "elm/time": "1.0.0",
+      "elm/url": "1.0.0",
+      "elm/virtual-dom": "1.0.2",
+      "justinmimbs/date": "4.1.0",
+      "justinmimbs/time-extra": "1.2.0",
+      "ryan-haskell/date-format": "1.0.0",
+      "terezka/intervals": "2.0.2"
     }
+  },
+  "test-dependencies": {
+    "direct": {
+      "elm-explorations/test": "2.2.0"
+    },
+    "indirect": {
+      "elm/random": "1.0.0"
+    }
+  }
 }

+ 79 - 11
js/app.js

@@ -1,14 +1,82 @@
-import setData from "../data/mkm.json";
-import draftData from "../data/draft_mkm.json";
-import cardRatings from "../data/card-ratings.json";
+import draftData from "../data/draft_okr_bad.json";
 
 import {Elm} from '../src/Main.elm';
 
-Elm.Main.init({
-    node: document.getElementById('myapp'),
-    flags: {
-        setData: JSON.stringify(setData),
-        draftData: JSON.stringify(draftData),
-        cardRatings: JSON.stringify(cardRatings)
-    }
-});
+function getSetData(db, setCode, callback) {
+    const transaction = db.transaction(["sets"], "readwrite");
+
+    const objectStore = transaction.objectStore("sets");
+    const request = objectStore.get(setCode);
+
+    request.onerror = (event) => {
+        alert("Database error")
+    };
+    request.onsuccess = (event) => {
+        if (request.result === undefined) {
+            callback({code: setCode, data: null});
+        } else {
+            callback({code: setCode, data: request.result.data});
+        }
+    };
+}
+
+function saveSetData(db, setData) {
+    const transaction = db.transaction(["sets"], "readwrite");
+
+    const objectStore = transaction.objectStore("sets");
+    const request = objectStore.add(setData);
+
+    request.onerror = (event) => {
+        alert("Database error")
+    };
+}
+
+function getAllLocalSets(db, callback) {
+    const transaction = db.transaction(["sets"], "readwrite");
+
+    const objectStore = transaction.objectStore("sets");
+
+    objectStore.getAll().onsuccess = (event) => {
+        callback(event.target.result.map((set) => set.code));
+    };
+
+}
+
+const openDBRequest = indexedDB.open("set_database", 2);
+
+openDBRequest.onerror = (event) => {
+    alert("Could not open browser database");
+};
+
+openDBRequest.onupgradeneeded = (event) => {
+    console.log("Upgrading")
+    const database = event.target.result;
+
+    const objectStore = database.createObjectStore("sets", {keyPath: "code"});
+}
+
+openDBRequest.onsuccess = (event) => {
+    const database = event.target.result;
+
+    getAllLocalSets(database, (sets) => {
+        const app = Elm.Main.init({
+            node: document.getElementById('myapp'),
+            flags: {
+                sets: sets,
+                draftData: JSON.stringify(draftData),
+            }
+        });
+
+        app.ports.sendDoesSetHaveLocalData.subscribe((setCode) => {
+            getSetData(database, setCode, (data) => {
+                app.ports.receiveDoesSetHaveLocalData.send(JSON.stringify(data));
+            })
+        });
+
+        app.ports.sendSaveLocalData.subscribe((setData) => {
+            saveSetData(database, setData);
+        })
+
+    })
+};
+

+ 34 - 0
server/main.py

@@ -2,9 +2,25 @@ import json
 import os
 
 from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
 
 app = FastAPI()
 
+origins = [
+    "http://drafter.cadel.me",
+    "https://drafter.cadel.me",
+    "http://localhost",
+    "http://localhost:1234",
+]
+
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=origins,
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
 
 def discover_sets():
     # find all set directories
@@ -46,3 +62,21 @@ def read_item(set_name: str):
         card_data = json.load(f)
 
     return {"data": card_data}
+
+
+@app.get("/sets/{set_name}")
+def read_item(set_name: str):
+    if set_name not in discover_sets():
+        return {"error": "Set not found"}
+
+    card_data_path = os.path.join("data", "sets", set_name, "card-data.json")
+
+    with open(card_data_path) as f:
+        card_data = json.load(f)
+
+    card_ratings_path = os.path.join("data", "sets", set_name, "card-ratings-all.json")
+
+    with open(card_ratings_path) as f:
+        ratings_data = json.load(f)
+
+    return {"code": set_name, "data": {"cards": card_data, "ratings": ratings_data}}

+ 59 - 0
src/API.elm

@@ -0,0 +1,59 @@
+module API exposing (getSetData, getSets)
+
+import Database exposing (Database)
+import Http
+import Json.Decode as Decode
+
+
+apiUrl : String
+apiUrl =
+    "http://localhost:8000"
+
+
+makeApiUrl : List String -> String
+makeApiUrl paths =
+    apiUrl ++ "/" ++ String.join "/" paths
+
+
+getSets : (Result String (List String) -> msg) -> Cmd msg
+getSets onSuccess =
+    Http.get
+        { url = makeApiUrl [ "sets" ]
+        , expect =
+            Http.expectJson (Result.mapError httpErrorToString >> onSuccess)
+                (Decode.field "sets"
+                    (Decode.list Decode.string)
+                )
+        }
+
+
+getSetData : String -> (Result String ( String, Maybe Database ) -> msg) -> Cmd msg
+getSetData setCode onSuccess =
+    Http.request
+        { method = "GET"
+        , headers = []
+        , url = makeApiUrl [ "sets", setCode ]
+        , body = Http.emptyBody
+        , expect = Http.expectJson (Result.mapError httpErrorToString >> onSuccess) Database.decoder
+        , timeout = Nothing
+        , tracker = Just setCode
+        }
+
+
+httpErrorToString : Http.Error -> String
+httpErrorToString error =
+    case error of
+        Http.BadUrl url ->
+            "Bad URL: " ++ url
+
+        Http.Timeout ->
+            "Timeout"
+
+        Http.NetworkError ->
+            "Network error"
+
+        Http.BadStatus status ->
+            "Bad status: " ++ String.fromInt status
+
+        Http.BadBody body ->
+            "Bad body: " ++ body

+ 1 - 0
src/Card.elm

@@ -61,6 +61,7 @@ type alias CardDetails =
     , power : Maybe Power
     , toughness : Maybe Power
     , colors : List ManaColor
+    , rawManaCost : String
     , manaCost : Maybe (List ManaCost)
     , imageUrl : String
     }

+ 27 - 0
src/Components/Button.elm

@@ -0,0 +1,27 @@
+module Components.Button exposing (make, view)
+
+import Html exposing (Html)
+import Html.Attributes exposing (class)
+import Html.Events exposing (onClick)
+
+
+type alias ButtonConfig msg =
+    { label : String
+    , onClick : msg
+    }
+
+
+make : String -> msg -> ButtonConfig msg
+make label onClick =
+    { label = label
+    , onClick = onClick
+    }
+
+
+view : ButtonConfig msg -> Html msg
+view cfg =
+    Html.button
+        [ class "bg-slate-900 rounded text-white p-2 shadow disabled:opacity-50 hover:bg-slate-700"
+        , onClick cfg.onClick
+        ]
+        [ Html.text cfg.label ]

+ 149 - 12
src/Database.elm

@@ -1,9 +1,10 @@
-module Database exposing (Database, decode, get, getAll)
+module Database exposing (Database, decode, decoder, encode, fromCardData, get, getAll)
 
 import Card exposing (CardData, CardDetails, CardPerformanceData, CardType(..), ManaColor(..), Power(..), parseManaCost)
 import Dict exposing (Dict)
 import Json.Decode as Decode exposing (Decoder, decodeString)
 import Json.Decode.Pipeline exposing (optional, required)
+import Json.Encode as Encode
 import Tuple exposing (pair)
 
 
@@ -11,6 +12,13 @@ type Database
     = Database (Dict String CardData)
 
 
+fromCardData : List CardData -> Database
+fromCardData cards =
+    List.map (\card -> ( card.details.name, card )) cards
+        |> Dict.fromList
+        |> Database
+
+
 get : String -> Database -> Maybe CardData
 get name (Database db) =
     Dict.get name db
@@ -21,20 +29,44 @@ getAll (Database db) =
     Dict.values db
 
 
-decode : String -> String -> Result String Database
-decode setData draftData =
-    case ( decodeString decodeSetData setData, decodeString decodePerformanceData draftData ) of
-        ( Ok set, Ok draft ) ->
-            Ok (createDatabase draft set |> Database)
+decode : String -> Result String ( String, Maybe Database )
+decode setData =
+    decodeString decoder setData |> Result.mapError Decode.errorToString
+
+
+decoder : Decoder ( String, Maybe Database )
+decoder =
+    Decode.map2 pair
+        (Decode.field "code" Decode.string)
+        (Decode.field "data"
+            (Decode.nullable
+                (Decode.map2 createDatabase
+                    (Decode.field "ratings" decodePerformanceData)
+                    (Decode.field "cards" decodeSetData)
+                )
+            )
+        )
+
+
+databaseToDict : Database -> Dict String CardData
+databaseToDict (Database db) =
+    db
 
-        ( Err err, _ ) ->
-            Err <| Decode.errorToString err
 
-        ( _, Err err ) ->
-            Err <| Decode.errorToString err
+encode : ( String, Database ) -> Encode.Value
+encode ( setCode, Database db ) =
+    Encode.object
+        [ ( "code", Encode.string setCode )
+        , ( "data"
+          , Encode.object
+                [ ( "ratings", Encode.list encodeCardPerformance (Dict.values db) )
+                , ( "cards", Encode.list encodeCardDetails (Dict.values db) )
+                ]
+          )
+        ]
 
 
-createDatabase : Dict String CardPerformanceData -> Dict String CardDetails -> Dict String CardData
+createDatabase : Dict String CardPerformanceData -> Dict String CardDetails -> Database
 createDatabase performanceData detailsData =
     Dict.merge
         (\_ _ cardData -> cardData)
@@ -43,13 +75,13 @@ createDatabase performanceData detailsData =
         performanceData
         detailsData
         Dict.empty
+        |> Database
 
 
 decodePerformanceData : Decoder (Dict String CardPerformanceData)
 decodePerformanceData =
     Decode.list decodeCardPerformance
         |> Decode.map Dict.fromList
-        |> Decode.map (Debug.log "perf")
 
 
 decodeSetData : Decoder (Dict String CardDetails)
@@ -58,6 +90,28 @@ decodeSetData =
         |> Decode.map Dict.fromList
 
 
+encodeCardPerformance : CardData -> Encode.Value
+encodeCardPerformance d =
+    Encode.object
+        [ ( "Name", Encode.string d.details.name )
+        , ( "# Seen", encodeIntString d.performance.totalTimesSeen )
+        , ( "# Picked", encodeIntString d.performance.totalTimesPicked )
+        , ( "ATA", encodeMaybeFloatString d.performance.averagePickPosition )
+        , ( "ALSA", encodeMaybeFloatString d.performance.averageSeenPosition )
+        , ( "GIH WR", encodeMaybePercentageString d.performance.gameInHandWinRate )
+        , ( "IWD"
+          , Encode.string
+                (case d.performance.improvementWhenDrawn of
+                    Just i ->
+                        String.fromFloat i ++ "pp"
+
+                    Nothing ->
+                        ""
+                )
+          )
+        ]
+
+
 decodeCardPerformance : Decoder ( String, CardPerformanceData )
 decodeCardPerformance =
     Decode.map2 pair
@@ -90,6 +144,11 @@ decodeImprovementWhenDrawn =
             )
 
 
+encodeIntString : Int -> Encode.Value
+encodeIntString i =
+    Encode.string (String.fromInt i)
+
+
 decodeIntString : Decoder Int
 decodeIntString =
     Decode.string
@@ -104,6 +163,16 @@ decodeIntString =
             )
 
 
+encodeMaybeFloatString : Maybe Float -> Encode.Value
+encodeMaybeFloatString m =
+    case m of
+        Just f ->
+            Encode.string (String.fromFloat f)
+
+        Nothing ->
+            Encode.string ""
+
+
 decodeMaybeFloatString : Decoder (Maybe Float)
 decodeMaybeFloatString =
     Decode.string
@@ -122,6 +191,16 @@ decodeMaybeFloatString =
             )
 
 
+encodeMaybePercentageString : Maybe Float -> Encode.Value
+encodeMaybePercentageString m =
+    case m of
+        Just f ->
+            Encode.string (String.fromFloat (f * 100) ++ "%")
+
+        Nothing ->
+            Encode.string ""
+
+
 decodeMaybePercentageString : Decoder (Maybe Float)
 decodeMaybePercentageString =
     Decode.string
@@ -140,6 +219,28 @@ decodeMaybePercentageString =
             )
 
 
+encodeCardDetails : CardData -> Encode.Value
+encodeCardDetails card =
+    Encode.object
+        [ ( "name", Encode.string card.details.name )
+        , ( "cmc", Encode.int card.details.cmc )
+        , ( "type_line", Encode.string card.details.typeLine )
+        , ( "oracle_text"
+          , case card.details.oracleText of
+                Just t ->
+                    Encode.string t
+
+                Nothing ->
+                    Encode.null
+          )
+        , ( "power", encodePower card.details.power )
+        , ( "toughness", encodePower card.details.toughness )
+        , ( "colors", Encode.list encodeManaColor card.details.colors )
+        , ( "mana_cost", Encode.string card.details.rawManaCost )
+        , ( "image_uris", Encode.object [ ( "large", Encode.string card.details.imageUrl ) ] )
+        ]
+
+
 decodeCardDetails : Decoder ( String, CardDetails )
 decodeCardDetails =
     Decode.map2 pair
@@ -153,6 +254,7 @@ decodeCardDetails =
             |> optional "power" (Decode.nullable decodePower) Nothing
             |> optional "toughness" (Decode.nullable decodePower) Nothing
             |> required "colors" (Decode.list decodeManaColor)
+            |> required "mana_cost" Decode.string
             |> required "mana_cost"
                 (Decode.string
                     |> Decode.andThen
@@ -169,6 +271,19 @@ decodeCardDetails =
         )
 
 
+encodePower : Maybe Power -> Encode.Value
+encodePower p =
+    case p of
+        Just (ConstantPower i) ->
+            Encode.string (String.fromInt i)
+
+        Just VariablePower ->
+            Encode.string "*"
+
+        Nothing ->
+            Encode.null
+
+
 decodePower : Decoder Power
 decodePower =
     Decode.string
@@ -187,6 +302,28 @@ decodePower =
             )
 
 
+encodeManaColor : ManaColor -> Encode.Value
+encodeManaColor c =
+    case c of
+        Red ->
+            Encode.string "R"
+
+        Blue ->
+            Encode.string "U"
+
+        Green ->
+            Encode.string "G"
+
+        White ->
+            Encode.string "W"
+
+        Black ->
+            Encode.string "B"
+
+        Colorless ->
+            Encode.string "C"
+
+
 decodeManaColor : Decoder ManaColor
 decodeManaColor =
     Decode.string

+ 220 - 27
src/Main.elm

@@ -1,17 +1,22 @@
-module Main exposing (..)
+port module Main exposing (..)
 
+import API
 import Browser exposing (Document)
 import Card exposing (CardData, CardPerformanceDistributions, calculatePerformanceDistributions, manaCostToSymbol)
 import Chart as C
 import Chart.Attributes as CA
+import Components.Button as Button
 import Database
 import Deck
+import Dict exposing (Dict)
 import Draft exposing (Draft)
-import Html exposing (Html, a, button, div, img, li, span, text, ul)
+import Html exposing (Html, a, button, div, img, li, p, span, text, ul)
 import Html.Attributes exposing (alt, class, classList, disabled, src)
 import Html.Events as Events exposing (onClick, onMouseEnter)
 import Html.Keyed as Keyed
 import Icon exposing (chevronDown, chevronUp)
+import Json.Decode exposing (decodeString)
+import Json.Encode as Encode
 import List.Extra as List
 import Round
 import Signals
@@ -25,12 +30,13 @@ main =
         { init = init
         , update = update
         , view = view
-        , subscriptions = \_ -> Sub.none
+        , subscriptions = subscriptions
         }
 
 
 type Model
-    = Ready ReadyModel
+    = ChooseSet ChooseSetModel
+    | Ready ReadyModel
     | Error ErrorModel
 
 
@@ -51,6 +57,19 @@ type alias ToolboxAccordion =
     { cmc : Bool, signals : Bool, signalsDelta : Bool, deckList : Bool }
 
 
+type SetLoadStatus
+    = CheckingLocalData
+    | NoLocalData
+    | HasLocalData Database.Database
+    | FetchingRemoteData
+
+
+type alias ChooseSetModel =
+    { draftData : String
+    , sets : Dict String SetLoadStatus
+    }
+
+
 type alias ReadyModel =
     { draft : Draft
     , database : Database.Database
@@ -69,10 +88,52 @@ type alias ErrorModel =
     }
 
 
-init : { setData : String, draftData : String, cardRatings : String } -> ( Model, Cmd Msg )
+init : { sets : List String, draftData : String } -> ( Model, Cmd Msg )
 init flags =
-    case ( Draft.decode flags.draftData, Database.decode flags.setData flags.cardRatings ) of
-        ( Ok draftData, Ok database ) ->
+    let
+        setStatus : Dict String SetLoadStatus
+        setStatus =
+            flags.sets
+                |> List.map (\s -> ( s, CheckingLocalData ))
+                |> Dict.fromList
+
+        setCmds : List (Cmd Msg)
+        setCmds =
+            List.map
+                (\s -> sendDoesSetHaveLocalData s)
+                flags.sets
+    in
+    ( ChooseSet
+        { draftData = flags.draftData
+        , sets = setStatus
+        }
+    , Cmd.batch (API.getSets IOGotSets :: setCmds)
+    )
+
+
+type Msg
+    = Increment
+    | Decrement
+    | Highlight String
+    | FlipHighlightedCard
+    | SetFocusStat FocusStat
+    | SetDeckProgress DeckProgress
+    | ToggleCMCChart
+    | ToggleSignalsChart
+    | ToggleSignalsDeltaChart
+    | ToggleDeckList
+    | SetDeckSortMethod Deck.DeckSortMethod
+    | IOGotSets (Result String (List String))
+    | IOFetchSetData String
+    | IOGotSetData (Result String ( String, Maybe Database.Database ))
+    | PortReceiveDoesSetHaveLocalData String
+    | StartDraft String Database.Database
+
+
+startDraft : ChooseSetModel -> Database.Database -> ( Model, Cmd Msg )
+startDraft initFlags database =
+    case Draft.decode initFlags.draftData of
+        Ok draftData ->
             ( Ready
                 { draft = draftData
                 , database = database
@@ -92,33 +153,90 @@ init flags =
             , Cmd.none
             )
 
-        ( Err err, Ok _ ) ->
+        Err err ->
             ( Error { error = "Error decoding draft data: " ++ err }, Cmd.none )
 
-        ( Ok _, Err err ) ->
-            ( Error { error = "Error decoding set data: " ++ err }, Cmd.none )
 
-        ( Err draftError, Err databaseError ) ->
-            ( Error { error = "Error decoding draft data: " ++ draftError ++ ", set data: " ++ databaseError }, Cmd.none )
+update : Msg -> Model -> ( Model, Cmd Msg )
+update msg model =
+    case model of
+        ChooseSet mdl ->
+            case msg of
+                StartDraft setCode database ->
+                    startDraft mdl database
 
+                IOGotSets (Ok remoteData) ->
+                    -- Merge any new sets into the local sets
+                    let
+                        remoteSets =
+                            remoteData
+                                |> List.map (\s -> ( s, CheckingLocalData ))
+                                |> Dict.fromList
+
+                        newRemoteSets =
+                            Dict.diff remoteSets mdl.sets
+
+                        setStatus : Dict String SetLoadStatus
+                        setStatus =
+                            Dict.union
+                                mdl.sets
+                                newRemoteSets
+
+                        remoteSetCmds : List (Cmd Msg)
+                        remoteSetCmds =
+                            List.map
+                                (\s -> sendDoesSetHaveLocalData s)
+                                (Dict.keys newRemoteSets)
+                    in
+                    ( ChooseSet { mdl | sets = setStatus }
+                    , Cmd.batch remoteSetCmds
+                    )
 
-type Msg
-    = Increment
-    | Decrement
-    | Highlight String
-    | FlipHighlightedCard
-    | SetFocusStat FocusStat
-    | SetDeckProgress DeckProgress
-    | ToggleCMCChart
-    | ToggleSignalsChart
-    | ToggleSignalsDeltaChart
-    | ToggleDeckList
-    | SetDeckSortMethod Deck.DeckSortMethod
+                IOGotSets (Err e) ->
+                    ( Error { error = "Error fetching sets (" ++ e ++ ")" }, Cmd.none )
 
+                PortReceiveDoesSetHaveLocalData unparsedData ->
+                    let
+                        markSetDataLoaded :
+                            String
+                            -> Database.Database
+                            -> Dict String SetLoadStatus
+                            -> Dict String SetLoadStatus
+                        markSetDataLoaded setCode db =
+                            Dict.insert setCode (HasLocalData db)
+
+                        markSetDataNotAvailable : String -> Dict String SetLoadStatus -> Dict String SetLoadStatus
+                        markSetDataNotAvailable setCode =
+                            Dict.insert setCode NoLocalData
+                    in
+                    case Database.decode unparsedData of
+                        Ok ( setCode, Just db ) ->
+                            ( ChooseSet { mdl | sets = markSetDataLoaded setCode db mdl.sets }, Cmd.none )
+
+                        Ok ( setCode, Nothing ) ->
+                            ( ChooseSet { mdl | sets = markSetDataNotAvailable setCode mdl.sets }
+                            , Cmd.none
+                            )
+
+                        Err e ->
+                            ( Error { error = "Error decoding local set data (" ++ e ++ ")" }, Cmd.none )
+
+                IOFetchSetData setCode ->
+                    ( ChooseSet { mdl | sets = Dict.insert setCode FetchingRemoteData mdl.sets }
+                    , API.getSetData setCode IOGotSetData
+                    )
+
+                IOGotSetData (Ok ( setCode, Just database )) ->
+                    ( ChooseSet { mdl | sets = Dict.insert setCode (HasLocalData database) mdl.sets }
+                    , sendSaveLocalData (Database.encode ( setCode, database ))
+                    )
+
+                IOGotSetData (Err e) ->
+                    ( Error { error = "Error fetching remote set data (" ++ e ++ ")" }, Cmd.none )
+
+                _ ->
+                    ( ChooseSet mdl, Cmd.none )
 
-update : Msg -> Model -> ( Model, Cmd Msg )
-update msg model =
-    case model of
         Ready mdl ->
             case msg of
                 Increment ->
@@ -176,6 +294,21 @@ update msg model =
                 SetDeckSortMethod method ->
                     ( Ready { mdl | deckSortOrder = method }, Cmd.none )
 
+                IOGotSets _ ->
+                    ( Ready mdl, Cmd.none )
+
+                IOGotSetData _ ->
+                    ( Ready mdl, Cmd.none )
+
+                IOFetchSetData _ ->
+                    ( Ready mdl, Cmd.none )
+
+                PortReceiveDoesSetHaveLocalData _ ->
+                    ( Ready mdl, Cmd.none )
+
+                StartDraft _ _ ->
+                    ( Ready mdl, Cmd.none )
+
         Error mdl ->
             ( Error mdl, Cmd.none )
 
@@ -185,6 +318,9 @@ view model =
     { title = "Drafter"
     , body =
         [ case model of
+            ChooseSet mdl ->
+                viewChooseSet mdl
+
             Ready mdl ->
                 viewReady mdl
 
@@ -194,6 +330,45 @@ view model =
     }
 
 
+viewChooseSet : ChooseSetModel -> Html Msg
+viewChooseSet model =
+    let
+        viewLoadStatus : String -> SetLoadStatus -> Html Msg
+        viewLoadStatus setCode s =
+            case s of
+                FetchingRemoteData ->
+                    text "Loading..."
+
+                CheckingLocalData ->
+                    text "Loading..."
+
+                NoLocalData ->
+                    Button.make "Download" (IOFetchSetData setCode) |> Button.view
+
+                HasLocalData database ->
+                    Button.make "Open" (StartDraft setCode database) |> Button.view
+    in
+    div [ class "w-full h-full bg-slate-100 flex justify-center items-center" ]
+        [ div [ class "max-w-2xl max-h-2xl bg-slate-500 rounded-lg p-6 shadow-xl" ]
+            [ ul [ class "divide-y divide-slate-600" ]
+                (List.map
+                    (\s ->
+                        li [ class "py-2 text-white flex gap-2 items-center space-between w-full" ] <|
+                            case Dict.get s model.sets of
+                                Just setStatus ->
+                                    [ span [ class "grow" ] [ text (String.toUpper s) ]
+                                    , viewLoadStatus s setStatus
+                                    ]
+
+                                Nothing ->
+                                    [ text "Loading..." ]
+                    )
+                    (Dict.keys model.sets)
+                )
+            ]
+        ]
+
+
 viewReady : ReadyModel -> Html Msg
 viewReady model =
     div [ class "grid grid-cols-12 gap-6 h-full bg-slate-100" ]
@@ -698,3 +873,21 @@ viewKeyedCard model wasChosen { name, frontImage, backImage } =
         , viewFocusStat focusStat
         ]
     )
+
+
+subscriptions : Model -> Sub Msg
+subscriptions _ =
+    receiveDoesSetHaveLocalData PortReceiveDoesSetHaveLocalData
+
+
+
+-- PORTS
+
+
+port sendDoesSetHaveLocalData : String -> Cmd msg
+
+
+port receiveDoesSetHaveLocalData : (String -> msg) -> Sub msg
+
+
+port sendSaveLocalData : Encode.Value -> Cmd msg

+ 122 - 0
tests/TestDatabase.elm

@@ -0,0 +1,122 @@
+module TestDatabase exposing (..)
+
+import Card exposing (CardData, CardDetails, CardPerformanceData, CardType(..), ManaColor(..), ManaCost(..), Power(..))
+import Database exposing (Database)
+import Expect
+import Fuzz exposing (Fuzzer)
+import Json.Decode as Decode
+import Json.Encode as Encode
+import Test exposing (Test, describe, fuzz)
+
+
+niceFloat : Fuzzer Float
+niceFloat =
+    Fuzz.floatRange 0 100
+
+
+fuzzDatabase : Fuzzer Database
+fuzzDatabase =
+    Fuzz.listOfLengthBetween 1 2 fuzzCardData
+        |> Fuzz.map Database.fromCardData
+
+
+fuzzCardData : Fuzzer CardData
+fuzzCardData =
+    Fuzz.string
+        |> Fuzz.andThen
+            (\cardName ->
+                Fuzz.constant CardData
+                    |> Fuzz.andMap (fuzzCardDetails cardName)
+                    |> Fuzz.andMap fuzzCardPerformance
+            )
+
+
+fuzzCardPerformance : Fuzzer CardPerformanceData
+fuzzCardPerformance =
+    Fuzz.constant CardPerformanceData
+        |> Fuzz.andMap Fuzz.int
+        |> Fuzz.andMap Fuzz.int
+        |> Fuzz.andMap (Fuzz.maybe niceFloat)
+        |> Fuzz.andMap (Fuzz.maybe niceFloat)
+        |> Fuzz.andMap (Fuzz.maybe (Fuzz.constant 0.5))
+        |> Fuzz.andMap (Fuzz.maybe niceFloat)
+
+
+fuzzCardDetails : String -> Fuzzer CardDetails
+fuzzCardDetails name =
+    Fuzz.constant (CardDetails name)
+        |> Fuzz.andMap Fuzz.int
+        |> Fuzz.andMap (Fuzz.constant Creature)
+        |> Fuzz.andMap (Fuzz.constant "Creature // Pirate")
+        |> Fuzz.andMap (Fuzz.maybe Fuzz.string)
+        |> Fuzz.andMap (Fuzz.maybe fuzzPower)
+        |> Fuzz.andMap (Fuzz.maybe fuzzPower)
+        |> Fuzz.andMap (Fuzz.list fuzzManaColor)
+        |> Fuzz.andMap (Fuzz.constant "{X}{R}")
+        |> Fuzz.andMap (Fuzz.constant (Just [ X, Color Red ]))
+        |> Fuzz.andMap Fuzz.string
+
+
+fuzzCardType : Fuzzer CardType
+fuzzCardType =
+    Fuzz.oneOfValues
+        [ Creature
+        , Instant
+        , Sorcery
+        , Enchantment
+        , Artifact
+        , Planeswalker
+        , Land
+        , Other
+        ]
+
+
+fuzzPower : Fuzzer Power
+fuzzPower =
+    Fuzz.oneOf
+        [ Fuzz.constant VariablePower
+        , Fuzz.int |> Fuzz.map ConstantPower
+        ]
+
+
+fuzzManaColor : Fuzzer ManaColor
+fuzzManaColor =
+    Fuzz.oneOfValues
+        [ Red
+        , Green
+        , Blue
+        , White
+        , Black
+        , Colorless
+        ]
+
+
+fuzzManaCost : Fuzzer ManaCost
+fuzzManaCost =
+    Fuzz.oneOf
+        [ Fuzz.constant X
+        , Fuzz.constant Y
+        , Fuzz.constant Tap
+        , Fuzz.constant Pay
+        , Fuzz.int |> Fuzz.map AnyColor
+        , fuzzManaColor |> Fuzz.map Color
+        , fuzzManaColor |> Fuzz.map ColorPay
+        , fuzzManaColor |> Fuzz.map TwoOrColor
+        , Fuzz.map2 Hybrid fuzzManaColor fuzzManaColor
+        , Fuzz.map2 HybridPay fuzzManaColor fuzzManaColor
+        ]
+
+
+suite : Test
+suite =
+    describe "Database"
+        [ fuzz fuzzDatabase "encoder / decoder round trip" <|
+            \database ->
+                let
+                    roundTrip =
+                        Database.encode ( "mkm", database )
+                            |> Encode.encode 0
+                            |> Decode.decodeString Database.decoder
+                in
+                Expect.equal roundTrip (Ok ( "mkm", Just database ))
+        ]

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov