feat: Finish chapter 7
parent
8d6cc1db71
commit
29f8fbc385
1198
PhotoGroove/app.js
1198
PhotoGroove/app.js
File diff suppressed because it is too large
Load Diff
|
@ -39,21 +39,7 @@
|
||||||
<script src="http://elm-in-action.com/pasta.js"></script>
|
<script src="http://elm-in-action.com/pasta.js"></script>
|
||||||
<script src="app.js"></script> <!-- PhotoGroove.elm will get compiled into app.js --!>
|
<script src="app.js"></script> <!-- PhotoGroove.elm will get compiled into app.js --!>
|
||||||
<script>
|
<script>
|
||||||
var app = Elm.PhotoGroove.init({
|
var app = Elm.PhotoFolders.init({ node: document.getElementById("app"), }); // Elm object comes from app.js
|
||||||
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);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue