@@ -0,0 +1,279 @@
1+port module Main exposing (main)
2+
3+import Animator
4+import Dict exposing (Dict)
5+import Element exposing (..)
6+import Element.Background as Background
7+import Element.Border as Border
8+import Element.Events as Events
9+import Element.Font as Font
10+import Element.Input as Input
11+import Element.Keyed as Keyed
12+import Heroicons.Outline
13+import Html.Attributes
14+import Html.Events
15+import Json.Decode as JD
16+import List.Extra
17+import Svg.Attributes exposing (opacity)
18+import Time exposing (Posix)
19+import Ur
20+import Ur.Cmd
21+import Ur.Deconstructor as D
22+import Ur.Run
23+import Ur.Sub
24+
25+
26+url : String
27+url =
28+ "http://localhost:8080"
29+
30+
31+type alias Model =
32+ { ship : Maybe String
33+ , search : String
34+ , initiatedSearch : Maybe String
35+ , searchResults : List SearchResult
36+ , resultTimelines : Dict String ( Animator.Timeline Int, Animator.Timeline Bool )
37+ }
38+
39+
40+type alias SearchResult =
41+ { title : String
42+ , link : String
43+ }
44+
45+
46+type Msg
47+ = Noop
48+ | GotShipName String
49+ | UpdateSearch String
50+ | Search String
51+ | UpdateSearchResults (List SearchResult)
52+ | OpenResult String
53+ | Tick Posix
54+
55+
56+main : Ur.Run.Program Model Msg
57+main =
58+ Ur.Run.document
59+ { init =
60+ ( { ship = Nothing
61+ , search = ""
62+ , initiatedSearch = Nothing
63+ , searchResults = []
64+ , resultTimelines = Dict.empty
65+ }
66+ , Cmd.batch
67+ [ Ur.logIn url "lidlut-tabwed-pillex-ridrup"
68+ |> Cmd.map (always Noop)
69+ , Ur.getShipName url |> Cmd.map (result (always Noop) GotShipName)
70+ ]
71+ |> Ur.Cmd.cmd
72+ )
73+ , update = update
74+ , view = \model -> { title = "%seax", body = [ view model |> layout [] ] }
75+ , subscriptions = \model -> Animator.toSubscription Tick model (animator model)
76+ , createEventSource = createEventSource
77+ , urbitSubscriptions =
78+ \{ initiatedSearch, ship } ->
79+ case ( initiatedSearch, ship ) of
80+ ( Just query, Just ship_ ) ->
81+ Ur.Sub.sink
82+ { app = "seax"
83+ , path = [ "search", query ]
84+ , ship = ship_
85+ , deconstructor =
86+ D.list (D.cell D.cord D.cord)
87+ |> D.map
88+ (List.map
89+ (\( title, link ) ->
90+ { title = title, link = link }
91+ )
92+ >> UpdateSearchResults
93+ )
94+ }
95+
96+ _ ->
97+ Ur.Sub.none
98+ , onEventSourceMsg = onEventSourceMessage
99+ , urbitUrl = \_ -> url
100+ }
101+
102+
103+view : Model -> Element Msg
104+view model =
105+ case model.initiatedSearch of
106+ Nothing ->
107+ column [ centerX, centerY, padding 8, spacing 16 ]
108+ [ el [ centerX, Font.size 67, Font.heavy ] (text "%seax")
109+ , searchView model
110+ ]
111+
112+ Just query ->
113+ column [ width fill, padding 8, spacing 16 ]
114+ [ searchView model
115+ , Keyed.column [ width fill ]
116+ (model.searchResults
117+ |> List.map
118+ (\{ title, link } ->
119+ ( link
120+ , column
121+ [ spacing 4
122+ , width fill
123+ , pointer
124+ , alpha
125+ (Animator.move
126+ (Dict.get link model.resultTimelines
127+ |> Maybe.map Tuple.second
128+ |> Maybe.withDefault (Animator.init False)
129+ )
130+ (\x ->
131+ if x then
132+ Animator.at 1
133+
134+ else
135+ Animator.at 0
136+ )
137+ )
138+ , Html.Attributes.style "top"
139+ ((Animator.move
140+ (Dict.get link model.resultTimelines
141+ |> Maybe.map Tuple.first
142+ |> Maybe.withDefault (Animator.init 0)
143+ )
144+ (\i -> toFloat i * 47 |> Animator.at)
145+ |> String.fromFloat
146+ )
147+ ++ "px"
148+ )
149+ |> htmlAttribute
150+ , Html.Attributes.style "position" "absolute" |> htmlAttribute
151+ ]
152+ [ [ text title ]
153+ |> paragraph
154+ [ Font.size 16
155+ , width fill
156+ , Html.Attributes.style "text-overflow" "ellipsis" |> htmlAttribute
157+ ]
158+ , [ text link ]
159+ |> paragraph
160+ [ Font.size 12
161+ , Html.Attributes.style "text-overflow" "ellipsis" |> htmlAttribute
162+ , width fill
163+ , Font.color (rgb 0.7 0.7 0.7)
164+ ]
165+ ]
166+ )
167+ )
168+ )
169+ ]
170+
171+
172+searchView : { a | search : String } -> Element Msg
173+searchView model =
174+ row [ spacing 8 ]
175+ [ Input.text []
176+ { onChange = UpdateSearch
177+ , placeholder = Nothing
178+ , text = model.search
179+ , label = Input.labelHidden "search"
180+ }
181+ , Input.button [ height fill ]
182+ { label =
183+ text "search"
184+ |> el
185+ [ Border.width 4
186+ , padding 4
187+ , Font.size 29
188+ , Font.bold
189+ , Border.color (rgb 0 0 0)
190+ , Background.color (rgb 0 0 0)
191+ , Font.color (rgb 1 1 1)
192+ , mouseOver
193+ [ Font.color (rgb 0 0 0)
194+ , Background.color (rgb 1 1 1)
195+ ]
196+ ]
197+ , onPress = Just (Search model.search)
198+ }
199+ ]
200+
201+
202+update : Msg -> Model -> ( Model, Ur.Cmd.Cmd Msg )
203+update msg model =
204+ case msg of
205+ Noop ->
206+ ( model, Ur.Cmd.none )
207+
208+ GotShipName ship ->
209+ ( { model | ship = Just ship }, Ur.Cmd.none )
210+
211+ UpdateSearch search ->
212+ ( { model | search = search }, Ur.Cmd.none )
213+
214+ Search query ->
215+ ( { model | initiatedSearch = Just query }, Ur.Cmd.none )
216+
217+ UpdateSearchResults results ->
218+ let
219+ foo =
220+ List.Extra.zip (List.range 0 (List.length results - 1)) results
221+ |> List.map
222+ (\( index, result_ ) ->
223+ ( result_.link
224+ , case Dict.get result_.link model.resultTimelines of
225+ Just timeline ->
226+ timeline |> Tuple.mapFirst (Animator.go Animator.slowly index)
227+
228+ Nothing ->
229+ ( Animator.init index, Animator.init False |> Animator.go Animator.slowly True )
230+ )
231+ )
232+ |> Dict.fromList
233+ in
234+ ( { model | resultTimelines = foo, searchResults = results }, Ur.Cmd.none )
235+
236+ OpenResult url_ ->
237+ ( model, Ur.Cmd.none )
238+
239+ Tick time ->
240+ ( Animator.update time (animator model) model, Ur.Cmd.none )
241+
242+
243+animator : Model -> Animator.Animator Model
244+animator model =
245+ List.foldl
246+ (\key anim ->
247+ anim
248+ |> Animator.watchingWith
249+ (\m -> Dict.get key m.resultTimelines |> Maybe.map Tuple.first |> Maybe.withDefault (Animator.init 0))
250+ (\index m ->
251+ { m | resultTimelines = m.resultTimelines |> Dict.update key (Maybe.map (\( _, x ) -> ( index, x ))) }
252+ )
253+ (always False)
254+ |> Animator.watchingWith
255+ (\m -> Dict.get key m.resultTimelines |> Maybe.map Tuple.second |> Maybe.withDefault (Animator.init False))
256+ (\shown m ->
257+ { m | resultTimelines = m.resultTimelines |> Dict.update key (Maybe.map (\( x, _ ) -> ( x, shown ))) }
258+ )
259+ (always False)
260+ -- |> Animator.watching
261+ )
262+ Animator.animator
263+ (Dict.keys model.resultTimelines)
264+
265+
266+result : (a -> c) -> (b -> c) -> Result a b -> c
267+result f g res =
268+ case res of
269+ Ok b ->
270+ g b
271+
272+ Err a ->
273+ f a
274+
275+
276+port createEventSource : String -> Cmd msg
277+
278+
279+port onEventSourceMessage : (JD.Value -> msg) -> Sub msg