Skip to content

Instantly share code, notes, and snippets.

@BraxtonI
Last active July 14, 2020 05:42
Show Gist options
  • Select an option

  • Save BraxtonI/1a9fc0c56a03254de908c97b6deb1250 to your computer and use it in GitHub Desktop.

Select an option

Save BraxtonI/1a9fc0c56a03254de908c97b6deb1250 to your computer and use it in GitHub Desktop.

Revisions

  1. BraxtonI revised this gist Jul 14, 2020. 4 changed files with 52 additions and 60 deletions.
    14 changes: 14 additions & 0 deletions Common.elm
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,14 @@
    module Common exposing (urlPrefix, Photo)


    urlPrefix : String
    urlPrefix =
    "http://elm-in-action.com/"


    type alias Photo =
    { title : String
    , url : String
    , size : Int
    , relatedUrls : List String
    }
    6 changes: 3 additions & 3 deletions Main.elm
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    module Main exposing (main)
    module Main exposing (main)

    import Browser exposing (Document)
    import Browser.Navigation as Nav
    @@ -8,7 +8,7 @@ import Html.Lazy exposing (lazy)
    import PhotoFolders as Folders
    import PhotoGallery as Gallery
    import Url exposing (Url)
    import Url.Parser as Parser exposing ((</>), Parser, s, string)
    import Url.Parser as Parser exposing ((</>), Parser, s)



    @@ -91,7 +91,7 @@ viewHeader page =

    links =
    ul []
    [ navLink Folders { url = "/", caption = "Folders" }
    [ navLink Folders { url = "/", caption = "Folders" }
    , navLink Gallery { url = "/gallery", caption = "Gallery" }
    ]

    72 changes: 30 additions & 42 deletions PhotoFolders.elm
    Original file line number Diff line number Diff line change
    @@ -1,39 +1,40 @@
    module PhotoFolders exposing (Model, Msg, update, view, init)
    module PhotoFolders exposing (Model, Msg, update, view, init)

    import Dict exposing (Dict)
    import Html exposing (..)
    import Html.Attributes exposing (class, href, src)
    import Html.Events exposing (onClick)
    import Common exposing (urlPrefix, Photo)
    import Dict exposing (Dict)
    import Html exposing (..)
    import Html.Attributes exposing (class, href, src)
    import Html.Events exposing (onClick)
    import Http
    import Json.Decode as Decode exposing (Decoder, int, list, string)
    import Json.Decode as Decode exposing (Decoder, int, list, string)
    import Json.Decode.Pipeline exposing (required)


    type Folder
    = Folder
    { name : String
    , photoUrls : List String
    { name : String
    , photoUrls : List String
    , subfolders : List Folder
    , expanded : Bool
    , expanded : Bool
    }


    type alias Model =
    { selectedPhotoUrl : Maybe String
    , photos : Dict String Photo
    , root : Folder
    , photos : Dict String Photo
    , root : Folder
    }


    initialModel : Model
    initialModel =
    { selectedPhotoUrl = Nothing
    , photos = Dict.empty
    , root =
    , photos = Dict.empty
    , root =
    Folder
    { name = "Loading..."
    , expanded = True
    , photoUrls = []
    { name = "Loading..."
    , expanded = True
    , photoUrls = []
    , subfolders = []
    }
    }
    @@ -61,7 +62,7 @@ modelDecoder =

    type Msg
    = SelectPhotoUrl String
    | LoadPage (Result Http.Error Model)
    | LoadPage (Result Http.Error Model)
    | ToggleExpanded FolderPath


    @@ -106,14 +107,6 @@ view model =
    ]


    type alias Photo =
    { title : String
    , size : Int
    , relatedUrls : List String
    , url : String
    }


    viewPhoto : String -> Html Msg
    viewPhoto url =
    a [ href ("/photos/" ++ url), class "photo", onClick (SelectPhotoUrl url) ]
    @@ -160,7 +153,7 @@ viewFolder path (Folder folder) =
    contents =
    List.append
    (List.indexedMap viewSubfolder folder.subfolders)
    (List.map viewPhoto folder.photoUrls)
    (List.map viewPhoto folder.photoUrls)
    in
    div [ class "folder expanded" ]
    [ folderLabel
    @@ -180,11 +173,6 @@ appendIndex index path =
    Subfolder subfolderIndex (appendIndex index remainingPath)


    urlPrefix : String
    urlPrefix =
    "http://elm-in-action.com/"


    type FolderPath
    = End
    | Subfolder Int FolderPath
    @@ -215,26 +203,26 @@ toggleExpanded path (Folder folder) =


    type alias JsonPhoto =
    { title : String
    , size : Int
    { title : String
    , size : Int
    , relatedUrls : List String
    }


    jsonPhotoDecoder : Decoder JsonPhoto
    jsonPhotoDecoder =
    Decode.succeed JsonPhoto
    |> required "title" string
    |> required "size" int
    |> 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
    , { url = url
    , size = json.size
    , title = json.title
    , relatedUrls = json.relatedUrls
    }
    )
    @@ -264,17 +252,17 @@ folderDecoder =
    folderFromJson : String -> Dict String Photo -> List Folder -> Folder
    folderFromJson name photos subfolders =
    Folder
    { name = name
    , expanded = True
    { name = name
    , expanded = True
    , subfolders = subfolders
    , photoUrls = Dict.keys photos
    , photoUrls = Dict.keys photos
    }


    modelPhotosDecoder : Decoder (Dict String Photo)
    modelPhotosDecoder =
    Decode.succeed modelPhotosFromJson
    |> required "photos" photosDecoder
    |> required "photos" photosDecoder
    |> required "subfolders" (Decode.lazy (\_ -> list modelPhotosDecoder))


    20 changes: 5 additions & 15 deletions PhotoGallery.elm
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,7 @@
    port module PhotoGallery exposing (init, Model, Msg, subscriptions, update, view, reload)

    import Browser
    import Common exposing (urlPrefix, Photo)
    import Html exposing (..)
    import Html.Attributes as Attr exposing (checked, class, classList, id, name, src, title, type_)
    import Html.Events exposing (on, onClick)
    @@ -60,11 +61,6 @@ initialModel =



    urlPrefix : String
    urlPrefix =
    "http://elm-in-action.com/"


    type Msg
    = ClickedPhoto String
    | ClickedSize ThumbnailSize
    @@ -167,19 +163,13 @@ type alias FilterOptions =
    }


    type alias Photo =
    { url : String
    , size : Int
    , title : String
    }


    photoDecoder : Decoder Photo
    photoDecoder =
    succeed Photo
    |> required "url" string
    |> required "size" int
    |> optional "title" string "(untitled)"
    |> optional "title" string "(untitled)"
    |> required "url" string
    |> required "size" int
    |> optional "requiredUrl" (list string) []


    type Status
  2. BraxtonI revised this gist Jul 13, 2020. 4 changed files with 665 additions and 0 deletions.
    60 changes: 60 additions & 0 deletions Index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,60 @@
    <!doctype html>
    <html>
    <head>
    <link rel="stylesheet" href="http://elm-in-action.com/styles.css">
    <link rel="stylesheet" href="http://elm-in-action.com/range-slider.css">
    <script src="http://elm-in-action.com/range-slider.js"></script>

    <script>
    class RangeSlider extends HTMLElement {
    connectedCallback() {
    var input = document.createElement("input");
    this.appendChild(input);

    var jsr = new JSR(input, {
    max: this.max,
    values: [this.val],
    sliders: 1,
    grid: false
    });

    var rangeSliderNode = this;

    jsr.addEventListener("update", function(elem, value) {
    var event = new CustomEvent("slide", {
    detail: {userSlidTo: value}
    });

    rangeSliderNode.dispatchEvent(event);
    });

    }
    }

    window.customElements.define("range-slider", RangeSlider);
    </script>

    </head>

    <body>
    <div id="app"></div>

    <script src="http://elm-in-action.com/pasta.js"></script>
    <script src="app.js"></script>
    <script>
    var app = Elm.Main.init({node: document.getElementById("app"), flags: Pasta.version});

    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>
    </body>
    </html>
    File renamed without changes.
    286 changes: 286 additions & 0 deletions PhotoFolders.elm
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,286 @@
    module PhotoFolders exposing (Model, Msg, update, view, init)

    import Dict exposing (Dict)
    import Html exposing (..)
    import Html.Attributes exposing (class, href, 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 : Maybe String -> ( Model, Cmd Msg )
    init selectedFilename =
    ( { initialModel | selectedPhotoUrl = selectedFilename }
    , Http.get
    { url = "http://elm-in-action.com/folders/list"
    , expect = Http.expectJson LoadPage modelDecoder
    }
    )


    modelDecoder : Decoder Model
    modelDecoder =
    Decode.map2
    (\photos root ->
    { photos = photos, root = root, selectedPhotoUrl = Nothing }
    )
    modelPhotosDecoder
    folderDecoder


    type Msg
    = SelectPhotoUrl String
    | LoadPage (Result Http.Error Model)
    | ToggleExpanded FolderPath


    update : Msg -> Model -> ( Model, Cmd Msg )
    update msg model =
    case msg of
    ToggleExpanded path ->
    ( { model | root = toggleExpanded path model.root }, Cmd.none )

    SelectPhotoUrl url ->
    ( { model | selectedPhotoUrl = Just url }, Cmd.none )

    LoadPage (Ok newModel) ->
    ( { newModel | selectedPhotoUrl = model.selectedPhotoUrl }, Cmd.none )

    LoadPage (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" ]
    [ viewFolder End model.root
    ]
    , div [ class "selected-photo" ] [ selectedPhoto ]
    ]


    type alias Photo =
    { title : String
    , size : Int
    , relatedUrls : List String
    , url : String
    }


    viewPhoto : String -> Html Msg
    viewPhoto url =
    a [ href ("/photos/" ++ url), class "photo", onClick (SelectPhotoUrl 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 =
    a [ href ("/photos/" ++ url) ]
    [ img
    [ class "related-photo"
    , onClick (SelectPhotoUrl 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 (ToggleExpanded 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
    319 changes: 319 additions & 0 deletions PhotoGallery.elm
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,319 @@
    port module PhotoGallery exposing (init, Model, Msg, subscriptions, update, view, reload)

    import Browser
    import Html exposing (..)
    import Html.Attributes as Attr exposing (checked, class, classList, id, name, src, title, type_)
    import Html.Events exposing (on, onClick)
    import Http
    import Json.Decode exposing (Decoder, at, string, int, list, succeed)
    import Json.Decode.Pipeline exposing (optional, required)
    import Json.Encode as Encode
    import Random



    main : Program Float Model Msg
    main =
    Browser.element
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
    }



    subscriptions : Model -> Sub Msg
    subscriptions model =
    activityChanges GotActivity



    init : Float -> ( Model, Cmd Msg )
    init flags =
    let
    activity =
    "Initializing Pasta v" ++ String.fromFloat flags
    in
    ( { initialModel | activity = activity }, initialCmd )


    type alias Model =
    { status : Status
    , activity : String
    , chosenSize : ThumbnailSize
    , hue : Int
    , ripple : Int
    , noise : Int
    }


    initialModel : Model
    initialModel =
    { status = Loading
    , activity = ""
    , chosenSize = Medium
    , hue = 5
    , ripple = 5
    , noise = 5
    }



    urlPrefix : String
    urlPrefix =
    "http://elm-in-action.com/"


    type Msg
    = ClickedPhoto String
    | ClickedSize ThumbnailSize
    | ClickedSurpriseMe
    | GotActivity String
    | GotPhotos (Result Http.Error (List Photo))
    | GotRandomPhoto Photo
    | SlidHue Int
    | SlidNoise Int
    | SlidRipple Int



    view : Model -> Html Msg
    view model =
    div [ class "content" ] <|
    case model.status of
    Loaded photos selectedUrl ->
    viewLoaded photos selectedUrl model

    Loading ->
    []

    Errored errorMessage ->
    [ text ("Error: " ++ errorMessage) ]


    viewLoaded : List Photo -> String -> Model -> List (Html Msg)
    viewLoaded photos selectedUrl model =
    [ button
    [ onClick ClickedSurpriseMe ]
    [ text "Surprise Me!" ]
    , div [ class "activity" ] [ text model.activity ]
    , div [ class "filters" ]
    [ viewFilter SlidHue "Hue" model.hue
    , viewFilter SlidRipple "Ripple" model.ripple
    , viewFilter SlidNoise "Noise" model.noise
    ]
    , h3 [] [ text "Thumbnail Size:" ]
    , div [ id "choose-size" ]
    (List.map (viewSizeChooser model.chosenSize) [ 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 selectedUrl thumb =
    img
    [ src (urlPrefix ++ thumb.url)
    , title (thumb.title ++ " [" ++ String.fromInt thumb.size ++ " KB]")
    , classList [ ( "selected", selectedUrl == thumb.url ) ]
    , onClick (ClickedPhoto thumb.url)
    ]
    []


    viewSizeChooser : ThumbnailSize -> ThumbnailSize -> Html Msg
    viewSizeChooser chosenSize size =
    label []
    [ input [ type_ "radio", name "size", onClick (ClickedSize size) ,
    if size == chosenSize then
    checked True
    else
    checked False
    ] []
    , text (sizeToString size)
    ]


    sizeToString : ThumbnailSize -> String
    sizeToString size =
    case size of
    Small ->
    "small"

    Medium ->
    "med"

    Large ->
    "large"


    type ThumbnailSize
    = Small
    | Medium
    | Large


    port setFilters : FilterOptions -> Cmd msg


    port activityChanges : (String -> msg) -> Sub msg


    type alias FilterOptions =
    { url : String
    , filters : List { name : String, amount : Float }
    }


    type alias Photo =
    { url : String
    , size : Int
    , title : String
    }


    photoDecoder : Decoder Photo
    photoDecoder =
    succeed Photo
    |> required "url" string
    |> required "size" int
    |> optional "title" string "(untitled)"


    type Status
    = Loading
    | Loaded (List Photo) String
    | Errored String



    update : Msg -> Model -> ( Model, Cmd Msg )
    update msg model =
    case msg of
    GotRandomPhoto photo ->
    applyFilters { model | status = selectUrl photo.url model.status }

    ClickedPhoto url ->
    applyFilters { model | status = selectUrl url model.status }

    ClickedSize size ->
    ( { model | chosenSize = size }, Cmd.none )

    ClickedSurpriseMe ->
    case model.status of
    Loaded (firstPhoto :: otherPhotos) _ ->
    Random.uniform firstPhoto otherPhotos
    |> Random.generate GotRandomPhoto
    |> Tuple.pair model

    Loaded [] _ ->
    ( model, Cmd.none )

    Loading ->
    ( model, Cmd.none )

    Errored errorMessage ->
    ( model, Cmd.none )

    GotActivity activity ->
    ( { model | activity = activity }, Cmd.none )

    GotPhotos (Ok photos) ->
    applyFilters
    { model
    | status =
    case photos of
    first :: rest ->
    Loaded photos first.url

    [] ->
    Loaded [] ""
    }

    GotPhotos (Err httpError) ->
    ( { model | status = Errored "Server error!" }, Cmd.none )

    SlidHue hue ->
    applyFilters { model | hue = hue }

    SlidRipple ripple ->
    applyFilters { model | ripple = ripple }

    SlidNoise noise ->
    applyFilters { model | noise = noise }


    applyFilters : Model -> ( Model, Cmd Msg )
    applyFilters model =
    case model.status of
    Loaded photos selectedUrl ->
    let
    filters =
    [ { name = "Hue", amount = toFloat model.hue / 11 }
    , { name = "Ripple", amount = toFloat model.ripple / 11 }
    , { name = "Noise", amount = toFloat model.noise / 11 }
    ]

    url =
    urlPrefix ++ "large/" ++ selectedUrl
    in
    ( model, setFilters { url = url, filters = filters } )

    Loading ->
    ( model, Cmd.none )

    Errored errorMessage ->
    ( model, Cmd.none )


    selectUrl : String -> Status -> Status
    selectUrl url status =
    case status of
    Loaded photos _ ->
    Loaded photos url

    Loading ->
    status

    Errored errorMessage ->
    status


    initialCmd : Cmd Msg
    initialCmd =
    Http.get
    { url = "http://elm-in-action.com/photos/list.json"
    , expect = Http.expectJson GotPhotos (list photoDecoder)
    }


    reload : Model -> (Model, Cmd Msg)
    reload newModel =
    applyFilters newModel


    viewFilter : (Int -> Msg) -> String -> Int -> Html Msg
    viewFilter toMsg name magnitude =
    div [ class "filter-slider" ]
    [ label [] [ text name ]
    , rangeSlider
    [ Attr.max "11"
    , Attr.property "val" (Encode.int magnitude)
    , onSlide toMsg
    ]
    []
    , label [] [ text (String.fromInt magnitude) ]
    ]


    rangeSlider attributes children =
    node "range-slider" attributes children


    onSlide : (Int -> msg) -> Attribute msg
    onSlide toMsg =
    at [ "detail", "userSlidTo" ] int
    |> Json.Decode.map toMsg
    |> on "slide"
  3. BraxtonI revised this gist Jul 13, 2020. No changes.
  4. BraxtonI created this gist Jul 13, 2020.
    248 changes: 248 additions & 0 deletions .elm
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,248 @@
    module Main exposing (main)

    import Browser exposing (Document)
    import Browser.Navigation as Nav
    import Html exposing (Html, a, footer, h1, li, nav, text, ul)
    import Html.Attributes exposing (classList, href)
    import Html.Lazy exposing (lazy)
    import PhotoFolders as Folders
    import PhotoGallery as Gallery
    import Url exposing (Url)
    import Url.Parser as Parser exposing ((</>), Parser, s, string)



    main : Program Float Model Msg
    main =
    Browser.application
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
    , onUrlChange = ChangedUrl
    , onUrlRequest = ClickedLink
    }



    init : Float -> Url -> Nav.Key -> ( Model, Cmd Msg )
    init version url key =
    updateUrl url
    { page = NotFound
    , galleryModel = Tuple.first (Gallery.init version)
    , foldersModel = Tuple.first (Folders.init Nothing)
    , key = key
    , version = version
    }


    type alias Model =
    { page : Page
    , galleryModel : Gallery.Model
    , foldersModel : Folders.Model
    , key : Nav.Key
    , version : Float
    }


    type Page
    = FoldersPage
    | GalleryPage
    | NotFound


    type Route
    = Gallery
    | Folders
    | SelectedPhoto String



    view : Model -> Document Msg
    view model =
    let
    content =
    case model.page of
    FoldersPage ->
    Folders.view model.foldersModel
    |> Html.map GotFoldersMsg

    GalleryPage ->
    Gallery.view model.galleryModel
    |> Html.map GotGalleryMsg

    NotFound ->
    text "Not Found"
    in
    { title = "Photo Groove, SPA Style"
    , body =
    [ lazy viewHeader model.page
    , content
    , viewFooter
    ]
    }


    viewHeader : Page -> Html Msg
    viewHeader page =
    let
    logo =
    h1 [] [ text "Photo Groove" ]

    links =
    ul []
    [ navLink Folders { url = "/", caption = "Folders" }
    , navLink Gallery { url = "/gallery", caption = "Gallery" }
    ]

    navLink : Route -> { url : String, caption : String } -> Html msg
    navLink route { url, caption } =
    li
    [ classList
    [ ( "active"
    , isActive { link = route, page = page }
    )
    ]
    ]
    [ a [ href url ] [ text caption ] ]
    in
    nav [] [ logo, links ]


    parser : Parser (Route -> a) a
    parser =
    Parser.oneOf
    [ Parser.map Folders Parser.top
    , Parser.map Gallery (s "gallery")
    , Parser.map SelectedPhoto (s "photos" </> Parser.string)
    ]


    isActive : { link : Route, page : Page } -> Bool
    isActive { link, page } =
    case ( link, page ) of
    ( Gallery , GalleryPage ) -> True

    ( Gallery , _ ) -> False

    ( Folders , FoldersPage ) -> True

    ( Folders , _ ) -> False

    ( SelectedPhoto _, _ ) -> False

    viewFooter : Html msg
    viewFooter =
    footer []
    [ text "One is never alone with a rubber duck. -Douglas Adams" ]



    update : Msg -> Model -> ( Model, Cmd Msg )
    update msg model =
    case msg of
    ClickedLink urlRequest ->
    case urlRequest of
    Browser.External href ->
    ( model, Nav.load href )

    Browser.Internal url ->
    ( model, Nav.pushUrl model.key (Url.toString url) )

    ChangedUrl url ->
    updateUrl url model

    GotFoldersMsg foldersMsg ->
    case model.page of
    FoldersPage ->
    toFolders model (Folders.update foldersMsg model.foldersModel)

    _ ->
    ( model, Cmd.none )

    GotGalleryMsg galleryMsg ->
    case model.page of
    GalleryPage ->
    toGallery model (Gallery.update galleryMsg model.galleryModel)

    _ ->
    ( model, Cmd.none )


    type Msg
    = ClickedLink Browser.UrlRequest
    | ChangedUrl Url
    | GotFoldersMsg Folders.Msg
    | GotGalleryMsg Gallery.Msg


    toFolders : Model -> ( Folders.Model, Cmd Folders.Msg ) -> ( Model, Cmd Msg )
    toFolders model ( folders, cmd ) =
    ( { model | page = FoldersPage, foldersModel = folders }
    , Cmd.map GotFoldersMsg cmd
    )


    toGallery : Model -> ( Gallery.Model, Cmd Gallery.Msg ) -> ( Model, Cmd Msg )
    toGallery model ( gallery, cmd ) =
    ( { model | page = GalleryPage, galleryModel = gallery }
    , Cmd.map GotGalleryMsg cmd
    )


    updateUrl : Url -> Model -> ( Model, Cmd Msg )
    updateUrl url model =
    case Parser.parse parser url of
    Just Gallery ->
    toGallery model <|
    let
    initGalleryModel =
    (Gallery.init model.version)

    in
    -- if the gallery has not been initialized, proceed with regular initialization
    -- if the gallery has been initialized, model.galleryModel has the previous build, and should be used
    -- to reload the page via Gallery.reload, which also called Gallery.applyFilters to load the main-canvas
    if model.galleryModel == Tuple.first initGalleryModel then
    initGalleryModel

    else
    Gallery.reload model.galleryModel

    Just Folders ->
    toFolders model <|
    let
    initFoldersModel =
    (Folders.init Nothing)

    in
    -- if the folders page has not been initialized, proceed with regular initialization
    -- if the folders page has been initialized, model.foldersModel has the previous build, and should be used instead
    if model.foldersModel == Tuple.first initFoldersModel then
    initFoldersModel

    else
    ( model.foldersModel, Cmd.none )

    Just (SelectedPhoto filename) ->
    let
    foldersModel = model.foldersModel

    in
    -- Update model.foldersModel.selectPhotoUrl to filename
    toFolders model ( { foldersModel | selectedPhotoUrl = (Just filename) }, Cmd.none )


    Nothing ->
    ( { model | page = NotFound }, Cmd.none )



    subscriptions : Model -> Sub Msg
    subscriptions model =
    case model.page of
    GalleryPage ->
    Gallery.subscriptions model.galleryModel
    |> Sub.map GotGalleryMsg
    _ ->
    Sub.none