feat: Finish chapter 7

main
Louis Pearson 2024-02-01 00:37:08 -07:00
parent 8d6cc1db71
commit 29f8fbc385
3 changed files with 648 additions and 824 deletions

File diff suppressed because it is too large Load Diff

View File

@ -39,21 +39,7 @@
<script src="http://elm-in-action.com/pasta.js"></script>
<script src="app.js"></script> <!-- PhotoGroove.elm will get compiled into app.js --!>
<script>
var app = Elm.PhotoGroove.init({
node: document.getElementById("app"),
flags: Pasta.version
}); // Elm object comes from app.js
app.ports.setFilters.subscribe(function(options) {
requestAnimationFrame(function() {
Pasta.apply(document.getElementById("main-canvas"), options);
});
});
Pasta.addActivityListener(function(activity) {
console.log("Got some activity to send to Elm:", activity);
app.ports.activityChanges.send(activity);
});
var app = Elm.PhotoFolders.init({ node: document.getElementById("app"), }); // Elm object comes from app.js
</script>
</body>
</html>

View File

@ -0,0 +1,258 @@
module PhotoFolders exposing (main)
import Browser
import Dict exposing (Dict)
import Html exposing (..)
import Html.Attributes exposing (class, src)
import Html.Events exposing (onClick)
import Http
import Json.Decode as Decode exposing (Decoder, int, list, string)
import Json.Decode.Pipeline exposing (required)
type Folder =
Folder
{ name : String
, photoUrls : List String
, subfolders : List Folder
, expanded : Bool
}
type alias Model =
{ selectedPhotoUrl : Maybe String
, photos : Dict String Photo
, root : Folder
}
initialModel : Model
initialModel =
{ selectedPhotoUrl = Nothing
, photos = Dict.empty
, root = Folder { name = "Loading...", expanded = True, photoUrls = [], subfolders = [] }
}
init : () -> ( Model, Cmd Msg )
init _ =
( initialModel
, Http.get
{ url = "http://elm-in-action.com/folders/list"
, expect = Http.expectJson GotInitialModel modelDecoder
}
)
modelDecoder : Decoder Model
modelDecoder =
Decode.map2
(\photos root ->
{ photos = photos, root = root, selectedPhotoUrl = Nothing }
)
modelPhotosDecoder
folderDecoder
type Msg
= ClickedPhoto String
| GotInitialModel (Result Http.Error Model)
| ClickedFolder FolderPath
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ClickedFolder path ->
( { model | root = toggleExpanded path model.root }, Cmd.none )
ClickedPhoto url ->
( { model | selectedPhotoUrl = Just url }, Cmd.none )
GotInitialModel (Ok newModel) ->
( newModel, Cmd.none )
GotInitialModel (Err _) ->
( model, Cmd.none )
view : Model -> Html Msg
view model =
let
photoByUrl : String -> Maybe Photo
photoByUrl url =
Dict.get url model.photos
selectedPhoto : Html Msg
selectedPhoto =
case Maybe.andThen photoByUrl model.selectedPhotoUrl of
Just photo ->
viewSelectedPhoto photo
Nothing ->
text ""
in
div [ class "content" ]
[ div [ class "folders" ]
[ h1 [] [ text "Folders" ]
, viewFolder End model.root
]
, div [ class "selected-photo"] [ selectedPhoto ]
]
main : Program () Model Msg
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = \_ -> Sub.none
}
type alias Photo =
{ title : String
, size : Int
, relatedUrls : List String
, url : String
}
viewPhoto : String -> Html Msg
viewPhoto url =
div [ class "photo", onClick (ClickedPhoto url) ]
[ text url ]
viewSelectedPhoto : Photo -> Html Msg
viewSelectedPhoto photo =
div
[ class "selected-photo" ]
[ h2 [] [ text photo.title]
, img [ src (urlPrefix ++ "photos/" ++ photo.url ++ "/full") ] []
, span [] [ text (String.fromInt photo.size ++ "KB") ]
, h3 [] [ text "Related" ]
, div [ class "related-photos" ]
(List.map viewRelatedPhoto photo.relatedUrls)
]
viewRelatedPhoto : String -> Html Msg
viewRelatedPhoto url =
img
[ class "related-photo"
, onClick (ClickedPhoto url)
, src (urlPrefix ++ "photos/" ++ url ++ "/thumb")
]
[]
viewFolder : FolderPath -> Folder -> Html Msg
viewFolder path (Folder folder) =
let
viewSubfolder : Int -> Folder -> Html Msg
viewSubfolder index subfolder =
viewFolder (appendIndex index path) subfolder
folderLabel =
label [ onClick (ClickedFolder path) ] [ text folder.name ]
in
if folder.expanded then
let
contents =
List.append
(List.indexedMap viewSubfolder folder.subfolders)
(List.map viewPhoto folder.photoUrls)
in
div [ class "folder expanded" ]
[ folderLabel
, div [ class "contents" ] contents
]
else
div [ class "folder collapsed" ] [ folderLabel ]
appendIndex : Int -> FolderPath -> FolderPath
appendIndex index path =
case path of
End ->
Subfolder index End
Subfolder subfolderIndex remainingPath ->
Subfolder subfolderIndex (appendIndex index remainingPath)
urlPrefix : String
urlPrefix =
"http://elm-in-action.com/"
type FolderPath
= End
| Subfolder Int FolderPath
toggleExpanded : FolderPath -> Folder -> Folder
toggleExpanded path (Folder folder) =
case path of
End ->
Folder { folder | expanded = not folder.expanded }
Subfolder targetIndex remainingPath ->
let
subfolders : List Folder
subfolders =
List.indexedMap transform folder.subfolders
transform : Int -> Folder -> Folder
transform currentIndex currentSubfolder =
if currentIndex == targetIndex then
toggleExpanded remainingPath currentSubfolder
else
currentSubfolder
in
Folder { folder | subfolders = subfolders }
type alias JsonPhoto =
{ title : String
, size : Int
, relatedUrls : List String
}
jsonPhotoDecoder : Decoder JsonPhoto
jsonPhotoDecoder =
Decode.succeed JsonPhoto
|> required "title" string
|> required "size" int
|> required "related_photos" (list string)
finishPhoto : ( String, JsonPhoto ) -> ( String, Photo )
finishPhoto ( url, json ) =
( url
, { url = url
, size = json.size
, title = json.title
, relatedUrls = json.relatedUrls
}
)
fromPairs : List ( String, JsonPhoto ) -> Dict String Photo
fromPairs pairs =
pairs
|> List.map finishPhoto
|> Dict.fromList
photosDecoder : Decoder (Dict String Photo)
photosDecoder =
Decode.keyValuePairs jsonPhotoDecoder
|> Decode.map fromPairs
folderDecoder : Decoder Folder
folderDecoder =
Decode.succeed folderFromJson
|> required "name" string
|> required "photos" photosDecoder
|> required "subfolders" (Decode.lazy (\_ -> list folderDecoder))
folderFromJson : String -> Dict String Photo -> List Folder -> Folder
folderFromJson name photos subfolders =
Folder
{ name = name
, expanded = True
, subfolders = subfolders
, photoUrls = Dict.keys photos
}
modelPhotosDecoder : Decoder (Dict String Photo)
modelPhotosDecoder =
Decode.succeed modelPhotosFromJson
|> required "photos" photosDecoder
|> required "subfolders" (Decode.lazy (\_ -> list modelPhotosDecoder))
modelPhotosFromJson : Dict String Photo -> List (Dict String Photo) -> Dict String Photo
modelPhotosFromJson folderPhotos subfolderPhotos =
List.foldl Dict.union folderPhotos subfolderPhotos