10 Angajamente 9013f992e7 ... 0ea68be7bd

Autor SHA1 Permisiunea de a trimite mesaje. Dacă este dezactivată, utilizatorul nu va putea trimite nici un fel de mesaj Data
  Cadel Watson 0ea68be7bd Style set selection 1 an în urmă
  Cadel Watson 1b132327b3 Test with OKR draft 1 an în urmă
  Cadel Watson 13a116bfa9 Start draft from set 1 an în urmă
  Cadel Watson afe3fbb2db Load initial sets from local storage 1 an în urmă
  Cadel Watson 313bbe98a9 Save set data to indexeddb 1 an în urmă
  Cadel Watson 6073ce4c02 Encode data ready for indexeddb 1 an în urmă
  Cadel Watson 6a97056901 Fetch and save set data from server 1 an în urmă
  Cadel Watson b0357858c8 Trigger fetch 1 an în urmă
  Cadel Watson 37d1ce9e01 Try to read set data from IndexedDB 1 an în urmă
  Cadel Watson 4fd777ea5a Load initial sets 1 an în urmă
10 a modificat fișierele cu 730 adăugiri și 87 ștergeri
  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

Fișier diff suprimat deoarece este prea mare
+ 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';
 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
 import os
 
 
 from fastapi import FastAPI
 from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
 
 
 app = FastAPI()
 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():
 def discover_sets():
     # find all set directories
     # find all set directories
@@ -46,3 +62,21 @@ def read_item(set_name: str):
         card_data = json.load(f)
         card_data = json.load(f)
 
 
     return {"data": card_data}
     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
     , power : Maybe Power
     , toughness : Maybe Power
     , toughness : Maybe Power
     , colors : List ManaColor
     , colors : List ManaColor
+    , rawManaCost : String
     , manaCost : Maybe (List ManaCost)
     , manaCost : Maybe (List ManaCost)
     , imageUrl : String
     , 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 Card exposing (CardData, CardDetails, CardPerformanceData, CardType(..), ManaColor(..), Power(..), parseManaCost)
 import Dict exposing (Dict)
 import Dict exposing (Dict)
 import Json.Decode as Decode exposing (Decoder, decodeString)
 import Json.Decode as Decode exposing (Decoder, decodeString)
 import Json.Decode.Pipeline exposing (optional, required)
 import Json.Decode.Pipeline exposing (optional, required)
+import Json.Encode as Encode
 import Tuple exposing (pair)
 import Tuple exposing (pair)
 
 
 
 
@@ -11,6 +12,13 @@ type Database
     = Database (Dict String CardData)
     = 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 : String -> Database -> Maybe CardData
 get name (Database db) =
 get name (Database db) =
     Dict.get name db
     Dict.get name db
@@ -21,20 +29,44 @@ getAll (Database db) =
     Dict.values 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 =
 createDatabase performanceData detailsData =
     Dict.merge
     Dict.merge
         (\_ _ cardData -> cardData)
         (\_ _ cardData -> cardData)
@@ -43,13 +75,13 @@ createDatabase performanceData detailsData =
         performanceData
         performanceData
         detailsData
         detailsData
         Dict.empty
         Dict.empty
+        |> Database
 
 
 
 
 decodePerformanceData : Decoder (Dict String CardPerformanceData)
 decodePerformanceData : Decoder (Dict String CardPerformanceData)
 decodePerformanceData =
 decodePerformanceData =
     Decode.list decodeCardPerformance
     Decode.list decodeCardPerformance
         |> Decode.map Dict.fromList
         |> Decode.map Dict.fromList
-        |> Decode.map (Debug.log "perf")
 
 
 
 
 decodeSetData : Decoder (Dict String CardDetails)
 decodeSetData : Decoder (Dict String CardDetails)
@@ -58,6 +90,28 @@ decodeSetData =
         |> Decode.map Dict.fromList
         |> 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 : Decoder ( String, CardPerformanceData )
 decodeCardPerformance =
 decodeCardPerformance =
     Decode.map2 pair
     Decode.map2 pair
@@ -90,6 +144,11 @@ decodeImprovementWhenDrawn =
             )
             )
 
 
 
 
+encodeIntString : Int -> Encode.Value
+encodeIntString i =
+    Encode.string (String.fromInt i)
+
+
 decodeIntString : Decoder Int
 decodeIntString : Decoder Int
 decodeIntString =
 decodeIntString =
     Decode.string
     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 : Decoder (Maybe Float)
 decodeMaybeFloatString =
 decodeMaybeFloatString =
     Decode.string
     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 : Decoder (Maybe Float)
 decodeMaybePercentageString =
 decodeMaybePercentageString =
     Decode.string
     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 : Decoder ( String, CardDetails )
 decodeCardDetails =
 decodeCardDetails =
     Decode.map2 pair
     Decode.map2 pair
@@ -153,6 +254,7 @@ decodeCardDetails =
             |> optional "power" (Decode.nullable decodePower) Nothing
             |> optional "power" (Decode.nullable decodePower) Nothing
             |> optional "toughness" (Decode.nullable decodePower) Nothing
             |> optional "toughness" (Decode.nullable decodePower) Nothing
             |> required "colors" (Decode.list decodeManaColor)
             |> required "colors" (Decode.list decodeManaColor)
+            |> required "mana_cost" Decode.string
             |> required "mana_cost"
             |> required "mana_cost"
                 (Decode.string
                 (Decode.string
                     |> Decode.andThen
                     |> 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 : Decoder Power
 decodePower =
 decodePower =
     Decode.string
     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 : Decoder ManaColor
 decodeManaColor =
 decodeManaColor =
     Decode.string
     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 Browser exposing (Document)
 import Card exposing (CardData, CardPerformanceDistributions, calculatePerformanceDistributions, manaCostToSymbol)
 import Card exposing (CardData, CardPerformanceDistributions, calculatePerformanceDistributions, manaCostToSymbol)
 import Chart as C
 import Chart as C
 import Chart.Attributes as CA
 import Chart.Attributes as CA
+import Components.Button as Button
 import Database
 import Database
 import Deck
 import Deck
+import Dict exposing (Dict)
 import Draft exposing (Draft)
 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.Attributes exposing (alt, class, classList, disabled, src)
 import Html.Events as Events exposing (onClick, onMouseEnter)
 import Html.Events as Events exposing (onClick, onMouseEnter)
 import Html.Keyed as Keyed
 import Html.Keyed as Keyed
 import Icon exposing (chevronDown, chevronUp)
 import Icon exposing (chevronDown, chevronUp)
+import Json.Decode exposing (decodeString)
+import Json.Encode as Encode
 import List.Extra as List
 import List.Extra as List
 import Round
 import Round
 import Signals
 import Signals
@@ -25,12 +30,13 @@ main =
         { init = init
         { init = init
         , update = update
         , update = update
         , view = view
         , view = view
-        , subscriptions = \_ -> Sub.none
+        , subscriptions = subscriptions
         }
         }
 
 
 
 
 type Model
 type Model
-    = Ready ReadyModel
+    = ChooseSet ChooseSetModel
+    | Ready ReadyModel
     | Error ErrorModel
     | Error ErrorModel
 
 
 
 
@@ -51,6 +57,19 @@ type alias ToolboxAccordion =
     { cmc : Bool, signals : Bool, signalsDelta : Bool, deckList : Bool }
     { 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 =
 type alias ReadyModel =
     { draft : Draft
     { draft : Draft
     , database : Database.Database
     , 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 =
 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
             ( Ready
                 { draft = draftData
                 { draft = draftData
                 , database = database
                 , database = database
@@ -92,33 +153,90 @@ init flags =
             , Cmd.none
             , Cmd.none
             )
             )
 
 
-        ( Err err, Ok _ ) ->
+        Err err ->
             ( Error { error = "Error decoding draft data: " ++ err }, Cmd.none )
             ( 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 ->
         Ready mdl ->
             case msg of
             case msg of
                 Increment ->
                 Increment ->
@@ -176,6 +294,21 @@ update msg model =
                 SetDeckSortMethod method ->
                 SetDeckSortMethod method ->
                     ( Ready { mdl | deckSortOrder = method }, Cmd.none )
                     ( 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 ->
             ( Error mdl, Cmd.none )
             ( Error mdl, Cmd.none )
 
 
@@ -185,6 +318,9 @@ view model =
     { title = "Drafter"
     { title = "Drafter"
     , body =
     , body =
         [ case model of
         [ case model of
+            ChooseSet mdl ->
+                viewChooseSet mdl
+
             Ready mdl ->
             Ready mdl ->
                 viewReady 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 : ReadyModel -> Html Msg
 viewReady model =
 viewReady model =
     div [ class "grid grid-cols-12 gap-6 h-full bg-slate-100" ]
     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
         , 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 ))
+        ]

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff