1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
port module Main exposing (..)
import Browser
import Html exposing (Html, button, div, text, br, input, img)
import Html.Attributes exposing (style, class, value, placeholder, src)
import Html.Events exposing (onInput, onClick)
import Html.Lazy exposing (lazy)
import List exposing (foldr)
import Url.Parser exposing (Parser, (</>), string, fragment)
import Protocol exposing (decodeState, encodeState)
import Json.Decode as D
main = Browser.document
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
-- subscribe to messages from the websocket
subscriptions : Model -> Sub Msg
subscriptions model = recvPort GotMessage
-- corresponding ports for the subscriptions
port recvPort : (String -> msg) -> Sub msg
port sendPort : String -> Cmd msg
{- STATE AND STATE TRANSITIONS -}
-- the main client state
type alias Model = { slide : Int, max : Int, prefix : String }
-- start at the first slide, for now just assume that we have ten slides
init : (String, Int) -> (Model, Cmd Msg)
init (url, max) = ( { slide = 0, max = max, prefix = url}, Cmd.none)
-- possible state transitions:
type Msg = GotMessage String
| NextSlide
| PrevSlide
-- update stuff
update : Msg -> Model -> (Model, Cmd Msg)
update msg model = case msg of
NextSlide -> updateSlide ((+) 1) model
PrevSlide -> updateSlide ((+) -1) model
GotMessage text ->
case decodeState text of
Nothing -> (model, Cmd.none)
Just newstate -> ({model | slide = newstate.state }, Cmd.none)
-- while changing slides we must take care not to end up in an invalid
-- state (where slide > max), so this logic gets its own function
tweakSlide : (Int -> Int) -> Model -> Model
tweakSlide f model = { model | slide = modBy model.max (f model.slide) }
updateSlide : (Int -> Int) -> Model -> (Model, Cmd Msg)
updateSlide f model =
let new = tweakSlide f model
in (new, sendPort (encodeState { state = new.slide }))
{- VIEW COMPONENTS -}
-- our body consists of a slide container and a couple controls
body : Model -> Html Msg
body model = div []
[ div [class "slides"] (slideView model.slide model.max model.prefix)
, div [class "controls"]
[ button [ onClick PrevSlide ] [ text "←" ]
, text (String.fromInt (model.slide+1))
, button [ onClick NextSlide ] [ text "→" ]
]
]
-- the slide view is just a list of img tags, one for each slide; note
-- that these are all loaded on startup, with visibility done through
-- z indices so we won't have lags while switching between them.
slideView : Int -> Int -> String -> List (Html Msg)
slideView i max prefix = mkInterval i max -- List.range 0 (max - 1)
|> List.map (\x -> (prefix ++ (padNum 2 (x+1)) ++ ".png", x == i))
|> List.map (\(path,t) -> img [src path, onTop t] [])
mkInterval : Int -> Int -> List Int
mkInterval i m = List.range (i-2) (i+2)
|> List.map (\x -> modBy m x)
onTop t = style "z-index" (String.fromInt (if t then 10 else 0))
-- format an int with prefixed 0's — apparently elm doesn't have a
-- standard function for that, but it's needed for slide filenames
padNum : Int -> Int -> String
padNum l num =
let str = String.fromInt num
in if (String.length str) > l then str else
(String.repeat (l - String.length str) "0") ++ str
view : Model -> Browser.Document Msg
view model = { title = "Picarones"
, body = [body model]
}
|