Compare commits
2 Commits
b6cb2a2750
...
29f8fbc385
Author | SHA1 | Date |
---|---|---|
Louis Pearson | 29f8fbc385 | |
Louis Pearson | 8d6cc1db71 |
|
@ -0,0 +1 @@
|
||||||
|
elm-stuff
|
1198
PhotoGroove/app.js
1198
PhotoGroove/app.js
File diff suppressed because it is too large
Load Diff
|
@ -23,7 +23,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test-dependencies": {
|
"test-dependencies": {
|
||||||
"direct": {},
|
"direct": {
|
||||||
|
"elm-explorations/test": "2.2.0"
|
||||||
|
},
|
||||||
"indirect": {}
|
"indirect": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -1,22 +1,36 @@
|
||||||
port module PhotoGroove exposing (main)
|
port module PhotoGroove exposing
|
||||||
|
( Model
|
||||||
|
, Msg(..)
|
||||||
|
, Photo
|
||||||
|
, Status(..)
|
||||||
|
, initialModel
|
||||||
|
, main
|
||||||
|
, photoDecoder
|
||||||
|
, photoFromUrl
|
||||||
|
, update
|
||||||
|
, urlPrefix
|
||||||
|
, view
|
||||||
|
)
|
||||||
|
|
||||||
import Array exposing (Array)
|
import Array exposing (Array)
|
||||||
import Browser
|
import Browser
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes as Attr exposing (class, classList, id, name, src, title, type_)
|
import Html.Attributes as Attr exposing (class, classList, id, name, src, title, type_)
|
||||||
import Html.Events exposing (onClick, on)
|
import Html.Events exposing (on, onClick)
|
||||||
import Http
|
import Http
|
||||||
import Json.Decode exposing (Decoder, at, int, list, string, succeed)
|
import Json.Decode exposing (Decoder, at, int, list, string, succeed)
|
||||||
import Json.Decode.Pipeline exposing (optional, required)
|
import Json.Decode.Pipeline exposing (optional, required)
|
||||||
import Json.Encode
|
import Json.Encode
|
||||||
import Random
|
import Random
|
||||||
|
|
||||||
|
|
||||||
urlPrefix : String
|
urlPrefix : String
|
||||||
urlPrefix =
|
urlPrefix =
|
||||||
"https://elm-in-action.com/"
|
"https://elm-in-action.com/"
|
||||||
|
|
||||||
type Msg
|
|
||||||
= ClickedPhoto String
|
type Msg
|
||||||
|
= ClickedPhoto String
|
||||||
| SetSize ThumbnailSize
|
| SetSize ThumbnailSize
|
||||||
| ClickedSurpriseMe
|
| ClickedSurpriseMe
|
||||||
| GotRandomPhoto Photo
|
| GotRandomPhoto Photo
|
||||||
|
@ -26,6 +40,7 @@ type Msg
|
||||||
| SlidRipple Int
|
| SlidRipple Int
|
||||||
| SlidNoise Int
|
| SlidNoise Int
|
||||||
|
|
||||||
|
|
||||||
view : Model -> Html Msg
|
view : Model -> Html Msg
|
||||||
view model =
|
view model =
|
||||||
div [ class "content" ] <|
|
div [ class "content" ] <|
|
||||||
|
@ -39,6 +54,7 @@ view model =
|
||||||
Errored errorMessage ->
|
Errored errorMessage ->
|
||||||
[ text ("Error: " ++ errorMessage) ]
|
[ text ("Error: " ++ errorMessage) ]
|
||||||
|
|
||||||
|
|
||||||
viewFilter : (Int -> Msg) -> String -> Int -> Html Msg
|
viewFilter : (Int -> Msg) -> String -> Int -> Html Msg
|
||||||
viewFilter toMsg name magnitude =
|
viewFilter toMsg name magnitude =
|
||||||
div [ class "filter-slider" ]
|
div [ class "filter-slider" ]
|
||||||
|
@ -52,36 +68,39 @@ viewFilter toMsg name magnitude =
|
||||||
, label [] [ text (String.fromInt magnitude) ]
|
, label [] [ text (String.fromInt magnitude) ]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
viewLoaded : List Photo -> String -> Model -> List (Html Msg)
|
viewLoaded : List Photo -> String -> Model -> List (Html Msg)
|
||||||
viewLoaded photos selectedUrl model =
|
viewLoaded photos selectedUrl model =
|
||||||
[ h1 [] [ text "Photo Groove" ]
|
[ h1 [] [ text "Photo Groove" ]
|
||||||
, button
|
, button
|
||||||
[ onClick ClickedSurpriseMe ]
|
[ onClick ClickedSurpriseMe ]
|
||||||
[ text "Surprise Me!" ]
|
[ text "Surprise Me!" ]
|
||||||
, div [ class "activity" ] [ text model.activity ]
|
, div [ class "activity" ] [ text model.activity ]
|
||||||
, div [ class "filters" ]
|
, div [ class "filters" ]
|
||||||
[ viewFilter SlidHue "Hue" model.hue
|
[ viewFilter SlidHue "Hue" model.hue
|
||||||
, viewFilter SlidRipple "Ripple" model.ripple
|
, viewFilter SlidRipple "Ripple" model.ripple
|
||||||
, viewFilter SlidNoise "Noise" model.noise
|
, viewFilter SlidNoise "Noise" model.noise
|
||||||
]
|
|
||||||
, h3 [] [ text "Thumbnail Size:" ]
|
|
||||||
, div [ id "choose-size" ]
|
|
||||||
(List.map viewSizeChooser [ Small, Medium, Large ])
|
|
||||||
, div [ id "thumbnails", class (sizeToString model.chosenSize) ]
|
|
||||||
(List.map (viewThumbnail selectedUrl) photos)
|
|
||||||
, canvas [ id "main-canvas", class "large" ] []
|
|
||||||
]
|
]
|
||||||
|
, h3 [] [ text "Thumbnail Size:" ]
|
||||||
|
, div [ id "choose-size" ]
|
||||||
|
(List.map viewSizeChooser [ Small, Medium, Large ])
|
||||||
|
, div [ id "thumbnails", class (sizeToString model.chosenSize) ]
|
||||||
|
(List.map (viewThumbnail selectedUrl) photos)
|
||||||
|
, canvas [ id "main-canvas", class "large" ] []
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
viewThumbnail : String -> Photo -> Html Msg
|
viewThumbnail : String -> Photo -> Html Msg
|
||||||
viewThumbnail selectedUrl thumb =
|
viewThumbnail selectedUrl thumb =
|
||||||
img
|
img
|
||||||
[ src (urlPrefix ++ thumb.url)
|
[ src (urlPrefix ++ thumb.url)
|
||||||
, title (thumb.title ++ " [" ++ String.fromInt thumb.size ++ " KB]")
|
, title (thumb.title ++ " [" ++ String.fromInt thumb.size ++ " KB]")
|
||||||
, classList [ ( "selected", selectedUrl == thumb.url ) ]
|
, classList [ ( "selected", selectedUrl == thumb.url ) ]
|
||||||
, onClick (ClickedPhoto thumb.url)
|
, onClick (ClickedPhoto thumb.url)
|
||||||
]
|
]
|
||||||
[]
|
[]
|
||||||
|
|
||||||
|
|
||||||
viewSizeChooser : ThumbnailSize -> Html Msg
|
viewSizeChooser : ThumbnailSize -> Html Msg
|
||||||
viewSizeChooser size =
|
viewSizeChooser size =
|
||||||
label []
|
label []
|
||||||
|
@ -89,35 +108,45 @@ viewSizeChooser size =
|
||||||
, text (sizeToString size)
|
, text (sizeToString size)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
sizeToString : ThumbnailSize -> String
|
sizeToString : ThumbnailSize -> String
|
||||||
sizeToString size =
|
sizeToString size =
|
||||||
case size of
|
case size of
|
||||||
Small ->
|
Small ->
|
||||||
"small"
|
"small"
|
||||||
Medium ->
|
|
||||||
|
Medium ->
|
||||||
"medium"
|
"medium"
|
||||||
Large ->
|
|
||||||
|
Large ->
|
||||||
"large"
|
"large"
|
||||||
|
|
||||||
|
|
||||||
type ThumbnailSize
|
type ThumbnailSize
|
||||||
= Small
|
= Small
|
||||||
| Medium
|
| Medium
|
||||||
| Large
|
| Large
|
||||||
|
|
||||||
|
|
||||||
port setFilters : FilterOptions -> Cmd msg
|
port setFilters : FilterOptions -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
port activityChanges : (String -> msg) -> Sub msg
|
port activityChanges : (String -> msg) -> Sub msg
|
||||||
|
|
||||||
|
|
||||||
type alias FilterOptions =
|
type alias FilterOptions =
|
||||||
{ url : String
|
{ url : String
|
||||||
, filters : List { name : String, amount : Float }
|
, filters : List { name : String, amount : Float }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type alias Photo =
|
type alias Photo =
|
||||||
{ url : String
|
{ url : String
|
||||||
, size : Int
|
, size : Int
|
||||||
, title : String
|
, title : String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
photoDecoder : Decoder Photo
|
photoDecoder : Decoder Photo
|
||||||
photoDecoder =
|
photoDecoder =
|
||||||
succeed Photo
|
succeed Photo
|
||||||
|
@ -125,11 +154,13 @@ photoDecoder =
|
||||||
|> required "size" int
|
|> required "size" int
|
||||||
|> optional "title" string "(untitled)"
|
|> optional "title" string "(untitled)"
|
||||||
|
|
||||||
|
|
||||||
type Status
|
type Status
|
||||||
= Loading
|
= Loading
|
||||||
| Loaded (List Photo) String
|
| Loaded (List Photo) String
|
||||||
| Errored String
|
| Errored String
|
||||||
|
|
||||||
|
|
||||||
type alias Model =
|
type alias Model =
|
||||||
{ status : Status
|
{ status : Status
|
||||||
, activity : String
|
, activity : String
|
||||||
|
@ -139,31 +170,32 @@ type alias Model =
|
||||||
, noise : Int
|
, noise : Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
update msg model =
|
update msg model =
|
||||||
case msg of
|
case msg of
|
||||||
GotActivity activity ->
|
GotActivity activity ->
|
||||||
( { model | activity = activity }, Cmd.none)
|
( { model | activity = activity }, Cmd.none )
|
||||||
|
|
||||||
GotPhotos (Ok photos) ->
|
GotPhotos (Ok photos) ->
|
||||||
case photos of
|
case photos of
|
||||||
first ::rest ->
|
first :: rest ->
|
||||||
applyFilters
|
applyFilters
|
||||||
{ model
|
{ model
|
||||||
| status =
|
| status =
|
||||||
case List.head photos of
|
case List.head photos of
|
||||||
Just photo ->
|
Just photo ->
|
||||||
Loaded photos photo.url
|
Loaded photos photo.url
|
||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
Loaded [] ""
|
Loaded [] ""
|
||||||
}
|
}
|
||||||
|
|
||||||
[] ->
|
[] ->
|
||||||
( { model | status = Errored "0 photos found"}, Cmd.none )
|
( { model | status = Errored "0 photos found" }, Cmd.none )
|
||||||
|
|
||||||
GotPhotos (Err _) ->
|
GotPhotos (Err _) ->
|
||||||
( model, Cmd.none )
|
( model, Cmd.none )
|
||||||
|
|
||||||
GotRandomPhoto photo ->
|
GotRandomPhoto photo ->
|
||||||
applyFilters { model | status = selectUrl photo.url model.status }
|
applyFilters { model | status = selectUrl photo.url model.status }
|
||||||
|
@ -174,7 +206,7 @@ update msg model =
|
||||||
SetSize size ->
|
SetSize size ->
|
||||||
( { model | chosenSize = size }, Cmd.none )
|
( { model | chosenSize = size }, Cmd.none )
|
||||||
|
|
||||||
ClickedSurpriseMe ->
|
ClickedSurpriseMe ->
|
||||||
case model.status of
|
case model.status of
|
||||||
Loaded (firstPhoto :: otherPhotos) _ ->
|
Loaded (firstPhoto :: otherPhotos) _ ->
|
||||||
( model
|
( model
|
||||||
|
@ -191,7 +223,7 @@ update msg model =
|
||||||
Errored errorMessage ->
|
Errored errorMessage ->
|
||||||
( model, Cmd.none )
|
( model, Cmd.none )
|
||||||
|
|
||||||
SlidHue hue ->
|
SlidHue hue ->
|
||||||
applyFilters { model | hue = hue }
|
applyFilters { model | hue = hue }
|
||||||
|
|
||||||
SlidRipple ripple ->
|
SlidRipple ripple ->
|
||||||
|
@ -200,19 +232,20 @@ update msg model =
|
||||||
SlidNoise noise ->
|
SlidNoise noise ->
|
||||||
applyFilters { model | noise = noise }
|
applyFilters { model | noise = noise }
|
||||||
|
|
||||||
|
|
||||||
applyFilters : Model -> ( Model, Cmd msg )
|
applyFilters : Model -> ( Model, Cmd msg )
|
||||||
applyFilters model =
|
applyFilters model =
|
||||||
case model.status of
|
case model.status of
|
||||||
Loaded photos selectedUrl ->
|
Loaded photos selectedUrl ->
|
||||||
let
|
let
|
||||||
filters =
|
filters =
|
||||||
[ { name = "Hue", amount = toFloat model.hue / 11}
|
[ { name = "Hue", amount = toFloat model.hue / 11 }
|
||||||
, { name = "Ripple", amount = toFloat model.ripple / 11}
|
, { name = "Ripple", amount = toFloat model.ripple / 11 }
|
||||||
, { name = "Noise", amount = toFloat model.noise / 11}
|
, { name = "Noise", amount = toFloat model.noise / 11 }
|
||||||
]
|
]
|
||||||
|
|
||||||
url =
|
url =
|
||||||
urlPrefix ++ "large/" ++ selectedUrl
|
urlPrefix ++ "large/" ++ selectedUrl
|
||||||
in
|
in
|
||||||
( model, setFilters { url = url, filters = filters } )
|
( model, setFilters { url = url, filters = filters } )
|
||||||
|
|
||||||
|
@ -222,16 +255,20 @@ applyFilters model =
|
||||||
Errored errorMessage ->
|
Errored errorMessage ->
|
||||||
( model, Cmd.none )
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
selectUrl : String -> Status -> Status
|
selectUrl : String -> Status -> Status
|
||||||
selectUrl url status =
|
selectUrl url status =
|
||||||
case status of
|
case status of
|
||||||
Loaded photos _ ->
|
Loaded photos _ ->
|
||||||
Loaded photos url
|
Loaded photos url
|
||||||
|
|
||||||
Loading ->
|
Loading ->
|
||||||
status
|
status
|
||||||
|
|
||||||
Errored errorMessage ->
|
Errored errorMessage ->
|
||||||
status
|
status
|
||||||
|
|
||||||
|
|
||||||
initialModel : Model
|
initialModel : Model
|
||||||
initialModel =
|
initialModel =
|
||||||
{ status = Loading
|
{ status = Loading
|
||||||
|
@ -240,7 +277,8 @@ initialModel =
|
||||||
, hue = 5
|
, hue = 5
|
||||||
, ripple = 5
|
, ripple = 5
|
||||||
, noise = 5
|
, noise = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
initialCmd : Cmd Msg
|
initialCmd : Cmd Msg
|
||||||
initialCmd =
|
initialCmd =
|
||||||
|
@ -249,6 +287,7 @@ initialCmd =
|
||||||
, expect = Http.expectJson GotPhotos (list photoDecoder)
|
, expect = Http.expectJson GotPhotos (list photoDecoder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
main : Program Float Model Msg
|
main : Program Float Model Msg
|
||||||
main =
|
main =
|
||||||
Browser.element
|
Browser.element
|
||||||
|
@ -258,24 +297,33 @@ main =
|
||||||
, subscriptions = subscriptions
|
, subscriptions = subscriptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
init : Float -> ( Model, Cmd Msg )
|
init : Float -> ( Model, Cmd Msg )
|
||||||
init flags =
|
init flags =
|
||||||
let
|
let
|
||||||
activity =
|
activity =
|
||||||
"Initializing Pasta v" ++ String.fromFloat flags
|
"Initializing Pasta v" ++ String.fromFloat flags
|
||||||
in
|
in
|
||||||
( { initialModel | activity = activity }, initialCmd )
|
( { initialModel | activity = activity }, initialCmd )
|
||||||
|
|
||||||
|
|
||||||
subscriptions : Model -> Sub Msg
|
subscriptions : Model -> Sub Msg
|
||||||
subscriptions model =
|
subscriptions model =
|
||||||
activityChanges GotActivity
|
activityChanges GotActivity
|
||||||
|
|
||||||
|
|
||||||
rangeSlider : List (Attribute msg) -> List (Html msg) -> Html msg
|
rangeSlider : List (Attribute msg) -> List (Html msg) -> Html msg
|
||||||
rangeSlider attributes children =
|
rangeSlider attributes children =
|
||||||
node "range-slider" attributes children
|
node "range-slider" attributes children
|
||||||
|
|
||||||
|
|
||||||
onSlide : (Int -> msg) -> Attribute msg
|
onSlide : (Int -> msg) -> Attribute msg
|
||||||
onSlide toMsg =
|
onSlide toMsg =
|
||||||
at [ "detail", "userSlidTo" ] int
|
at [ "detail", "userSlidTo" ] int
|
||||||
|> Json.Decode.map toMsg
|
|> Json.Decode.map toMsg
|
||||||
|> on "slide"
|
|> on "slide"
|
||||||
|
|
||||||
|
|
||||||
|
photoFromUrl : String -> Photo
|
||||||
|
photoFromUrl url =
|
||||||
|
{ url = url, size = 0, title = "" }
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
module PhotoGrooveTests exposing (..)
|
||||||
|
|
||||||
|
import Expect exposing (Expectation)
|
||||||
|
import Fuzz exposing (Fuzzer, int, list, string)
|
||||||
|
import Html.Attributes as Attr exposing (src)
|
||||||
|
import Json.Decode as Decode exposing (decodeValue)
|
||||||
|
import Json.Encode as Encode
|
||||||
|
import PhotoGroove
|
||||||
|
exposing
|
||||||
|
( Model
|
||||||
|
, Msg(..)
|
||||||
|
, Photo
|
||||||
|
, Status(..)
|
||||||
|
, initialModel
|
||||||
|
, photoFromUrl
|
||||||
|
, update
|
||||||
|
, urlPrefix
|
||||||
|
, view
|
||||||
|
)
|
||||||
|
import Test exposing (..)
|
||||||
|
import Test.Html.Event as Event
|
||||||
|
import Test.Html.Query as Query
|
||||||
|
import Test.Html.Selector exposing (attribute, tag, text)
|
||||||
|
|
||||||
|
|
||||||
|
decoderTest : Test
|
||||||
|
decoderTest =
|
||||||
|
fuzz2 string int "title defaults to (untitled)" <|
|
||||||
|
\url size ->
|
||||||
|
[ ( "url", Encode.string url )
|
||||||
|
, ( "size", Encode.int size )
|
||||||
|
]
|
||||||
|
|> Encode.object
|
||||||
|
|> decodeValue PhotoGroove.photoDecoder
|
||||||
|
|> Result.map .title
|
||||||
|
|> Expect.equal (Ok "(untitled)")
|
||||||
|
|
||||||
|
|
||||||
|
sliders : Test
|
||||||
|
sliders =
|
||||||
|
describe "Slider sets the desired field in the model"
|
||||||
|
[ testSlider "SlidHue" SlidHue .hue
|
||||||
|
, testSlider "SlidRipple" SlidRipple .ripple
|
||||||
|
, testSlider "SlidNoise" SlidNoise .noise
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
testSlider : String -> (Int -> Msg) -> (Model -> Int) -> Test
|
||||||
|
testSlider description toMsg amountFromModel =
|
||||||
|
fuzz int description <|
|
||||||
|
\amount ->
|
||||||
|
initialModel
|
||||||
|
|> update (toMsg amount)
|
||||||
|
|> Tuple.first
|
||||||
|
|> amountFromModel
|
||||||
|
|> Expect.equal amount
|
||||||
|
|
||||||
|
|
||||||
|
noPhotosNoThumbnails : Test
|
||||||
|
noPhotosNoThumbnails =
|
||||||
|
test "No thumbnails render when there are no photos to render." <|
|
||||||
|
\_ ->
|
||||||
|
initialModel
|
||||||
|
|> PhotoGroove.view
|
||||||
|
|> Query.fromHtml
|
||||||
|
|> Query.findAll [ tag "img" ]
|
||||||
|
|> Query.count (Expect.equal 0)
|
||||||
|
|
||||||
|
|
||||||
|
thumbnailRendered : String -> Query.Single msg -> Expectation
|
||||||
|
thumbnailRendered url query =
|
||||||
|
query
|
||||||
|
|> Query.findAll [ tag "img", attribute (Attr.src (urlPrefix ++ url)) ]
|
||||||
|
|> Query.count (Expect.atLeast 1)
|
||||||
|
|
||||||
|
|
||||||
|
thumbnailsWork : Test
|
||||||
|
thumbnailsWork =
|
||||||
|
fuzz urlFuzzer "URLs render as thumbnail" <|
|
||||||
|
\urls ->
|
||||||
|
let
|
||||||
|
thumbnailChecks : List (Query.Single msg -> Expectation)
|
||||||
|
thumbnailChecks =
|
||||||
|
List.map thumbnailRendered urls
|
||||||
|
in
|
||||||
|
{ initialModel | status = Loaded (List.map photoFromUrl urls) "" }
|
||||||
|
|> view
|
||||||
|
|> Query.fromHtml
|
||||||
|
|> Expect.all thumbnailChecks
|
||||||
|
|
||||||
|
|
||||||
|
urlFuzzer : Fuzzer (List String)
|
||||||
|
urlFuzzer =
|
||||||
|
Fuzz.intRange 1 5
|
||||||
|
|> Fuzz.map urlsFromCount
|
||||||
|
|
||||||
|
|
||||||
|
urlsFromCount : Int -> List String
|
||||||
|
urlsFromCount urlCount =
|
||||||
|
List.range 1 urlCount
|
||||||
|
|> List.map (\num -> String.fromInt num ++ ".png")
|
||||||
|
|
||||||
|
|
||||||
|
clickThumbnail : Test
|
||||||
|
clickThumbnail =
|
||||||
|
fuzz3 urlFuzzer string urlFuzzer "clicking a thumbnail selects it" <|
|
||||||
|
\urlsBefore urlToSelect urlsAfter ->
|
||||||
|
let
|
||||||
|
url =
|
||||||
|
urlToSelect ++ ".jpeg"
|
||||||
|
|
||||||
|
photos =
|
||||||
|
(urlsBefore ++ url :: urlsAfter)
|
||||||
|
|> List.map photoFromUrl
|
||||||
|
|
||||||
|
srcToClick =
|
||||||
|
urlPrefix ++ url
|
||||||
|
in
|
||||||
|
{ initialModel | status = Loaded photos "" }
|
||||||
|
|> view
|
||||||
|
|> Query.fromHtml
|
||||||
|
|> Query.find [ tag "img", attribute (Attr.src srcToClick) ]
|
||||||
|
|> Event.simulate Event.click
|
||||||
|
|> Event.expect (ClickedPhoto url)
|
Loading…
Reference in New Issue