1module UI.Sources.State exposing (..)
2
3import Alien
4import Browser.Navigation as Nav
5import Common
6import Conditional exposing (ifThenElse)
7import Coordinates
8import Dict
9import Dict.Ext as Dict
10import Html.Events.Extra.Mouse as Mouse
11import Json.Decode as Json
12import Json.Encode
13import Monocle.Lens as Lens
14import Notifications
15import Return exposing (andThen, return)
16import Sources exposing (..)
17import Sources.Encoding as Sources
18import Sources.Services as Services
19import Sources.Services.Dropbox
20import Sources.Services.Google
21import Tracks.Collection
22import UI.Common.State as Common
23import UI.Page as Page
24import UI.Ports as Ports
25import UI.Sources.ContextMenu as Sources
26import UI.Sources.Form as Form
27import UI.Sources.Page as Sources
28import UI.Sources.Query
29import UI.Sources.Types exposing (..)
30import UI.Tracks.State as Tracks
31import UI.Types as UI exposing (Manager, Model)
32import UI.User.State.Export as User
33
34
35
36-- 🌳
37
38
39formLens =
40 { get = .sourceForm
41 , set = \form m -> { m | sourceForm = form }
42 }
43
44
45formContextLens =
46 Lens.compose
47 formLens
48 { get = .context
49 , set = \context m -> { m | context = context }
50 }
51
52
53formStepLens =
54 Lens.compose
55 formLens
56 { get = .step
57 , set = \step m -> { m | step = step }
58 }
59
60
61
62-- 📣
63
64
65update : Msg -> Manager
66update msg =
67 case msg of
68 Bypass ->
69 Return.singleton
70
71 --
72 FinishedProcessingSource a ->
73 finishedProcessingSource a
74
75 FinishedProcessing ->
76 finishedProcessing
77
78 Process ->
79 process
80
81 ProcessSpecific a ->
82 processSpecific a
83
84 ReportProcessingError a ->
85 reportProcessingError a
86
87 ReportProcessingProgress a ->
88 reportProcessingProgress a
89
90 StopProcessing ->
91 stopProcessing
92
93 -----------------------------------------
94 -- Collection
95 -----------------------------------------
96 AddToCollection a ->
97 addToCollection a
98
99 RemoveFromCollection a ->
100 removeFromCollection a
101
102 UpdateSourceData a ->
103 updateSourceData a
104
105 -----------------------------------------
106 -- Form
107 -----------------------------------------
108 AddSourceUsingForm ->
109 addSourceUsingForm
110
111 EditSourceUsingForm ->
112 editSourceUsingForm
113
114 RenameSourceUsingForm ->
115 renameSourceUsingForm
116
117 ReturnToIndex ->
118 returnToIndex
119
120 SelectService a ->
121 selectService a
122
123 SetFormData a b ->
124 setFormData a b
125
126 TakeStep ->
127 takeStep
128
129 TakeStepBackwards ->
130 takeStepBackwards
131
132 -----------------------------------------
133 -- Individual
134 -----------------------------------------
135 SourceContextMenu a b ->
136 sourceContextMenu a b
137
138 ToggleActivation a ->
139 toggleActivation a
140
141 ToggleDirectoryPlaylists a ->
142 toggleDirectoryPlaylists a
143
144 ToggleProcessAutomatically ->
145 toggleProcessAutomatically
146
147
148
149-- 🔱
150
151
152addSourcesFromUrl : Manager
153addSourcesFromUrl model =
154 case UI.Sources.Query.sourcesFromUrl model.url of
155 [] ->
156 Return.singleton model
157
158 sources ->
159 sources
160 |> List.foldl
161 (\s -> andThen <| addToCollection s)
162 (Return.singleton model)
163 |> Return.command
164 (Nav.replaceUrl
165 model.navKey
166 (model.url.path ++ Page.toString model.page)
167 )
168
169
170finishedProcessing : Manager
171finishedProcessing model =
172 (case model.processingNotificationId of
173 Just id ->
174 Common.dismissNotification { id = id }
175
176 Nothing ->
177 Return.singleton
178 )
179 { model | processingContext = [] }
180
181
182finishedProcessingSource : Json.Value -> Manager
183finishedProcessingSource json model =
184 case Json.decodeValue Json.string json of
185 Ok sourceId ->
186 model.processingContext
187 |> List.filter (Tuple.first >> (/=) sourceId)
188 |> (\newContext -> { model | processingContext = newContext })
189 |> Return.singleton
190
191 Err _ ->
192 Return.singleton model
193
194
195process : Manager
196process model =
197 case sourcesToProcess model of
198 [] ->
199 Return.singleton model
200
201 toProcess ->
202 if model.isOnline then
203 processSpecific toProcess model
204
205 else
206 toProcess
207 |> List.filter Sources.worksOffline
208 |> (\s ->
209 case s of
210 [] ->
211 Return.singleton model
212
213 _ ->
214 processSpecific s model
215 )
216
217
218processSpecific : List Source -> Manager
219processSpecific toProcess model =
220 let
221 notification =
222 Notifications.stickyCasual "Processing sources ..."
223
224 notificationId =
225 Notifications.id notification
226
227 newNotifications =
228 List.filter
229 (\n -> Notifications.kind n /= Notifications.Error)
230 model.notifications
231
232 processingContext =
233 toProcess
234 |> List.sortBy (.data >> Dict.fetch "name" "")
235 |> List.map (\{ id } -> ( id, 0 ))
236
237 newModel =
238 { model
239 | notifications = newNotifications
240 , processingContext = processingContext
241 , processingError = Nothing
242 , processingNotificationId = Just notificationId
243 }
244 in
245 [ ( "origin"
246 , Json.Encode.string (Common.urlOrigin model.url)
247 )
248 , ( "sources"
249 , Json.Encode.list Sources.encode toProcess
250 )
251 ]
252 |> Json.Encode.object
253 |> Alien.broadcast Alien.ProcessSources
254 |> Ports.toBrain
255 |> return newModel
256 |> andThen (Common.showNotification notification)
257
258
259reportProcessingError : Json.Value -> Manager
260reportProcessingError json model =
261 case Json.decodeValue (Json.dict Json.string) json of
262 Ok dict ->
263 let
264 args =
265 { error = Dict.fetch "error" "" dict
266 , sourceId = Dict.fetch "sourceId" "" dict
267 }
268 in
269 []
270 |> Notifications.errorWithCode
271 ("Could not process the _"
272 ++ Dict.fetch "sourceName" "" dict
273 ++ "_ source. I got the following response from the source:"
274 )
275 (Dict.fetch "error" "missingError" dict)
276 |> Common.showNotificationWithModel
277 { model | processingError = Just args }
278
279 Err _ ->
280 "Could not decode processing error"
281 |> Notifications.stickyError
282 |> Common.showNotificationWithModel model
283
284
285reportProcessingProgress : Json.Value -> Manager
286reportProcessingProgress json model =
287 case
288 Json.decodeValue
289 (Json.map2
290 (\p s ->
291 { progress = p
292 , sourceId = s
293 }
294 )
295 (Json.field "progress" Json.float)
296 (Json.field "sourceId" Json.string)
297 )
298 json
299 of
300 Ok { progress, sourceId } ->
301 model.processingContext
302 |> List.map
303 (\( sid, pro ) ->
304 ifThenElse (sid == sourceId)
305 ( sid, progress )
306 ( sid, pro )
307 )
308 |> (\processingContext ->
309 { model | processingContext = processingContext }
310 )
311 |> Return.singleton
312
313 Err _ ->
314 "Could not decode processing progress"
315 |> Notifications.stickyError
316 |> Common.showNotificationWithModel model
317
318
319stopProcessing : Manager
320stopProcessing model =
321 case model.processingNotificationId of
322 Just notificationId ->
323 Alien.StopProcessing
324 |> Alien.trigger
325 |> Ports.toBrain
326 |> return
327 { model
328 | processingContext = []
329 , processingNotificationId = Nothing
330 }
331 |> andThen (Common.dismissNotification { id = notificationId })
332
333 Nothing ->
334 Return.singleton model
335
336
337
338-- COLLECTION
339
340
341addToCollection : Source -> Manager
342addToCollection unsuitableSource model =
343 let
344 source =
345 setProperId
346 (List.length model.sources + 1)
347 model.currentTime
348 unsuitableSource
349 in
350 { model | sources = model.sources ++ [ source ] }
351 |> User.saveSources
352 |> andThen (processSpecific [ source ])
353
354
355removeFromCollection : { sourceId : String } -> Manager
356removeFromCollection { sourceId } model =
357 model.sources
358 |> List.filter (.id >> (/=) sourceId)
359 |> (\c -> { model | sources = c })
360 |> Return.singleton
361 |> andThen User.saveSources
362 |> andThen (Tracks.removeBySourceId sourceId)
363
364
365updateSourceData : Json.Value -> Manager
366updateSourceData json model =
367 json
368 |> Sources.decode
369 |> Maybe.map
370 (\source ->
371 List.map
372 (\s ->
373 if s.id == source.id then
374 source
375
376 else
377 s
378 )
379 model.sources
380 )
381 |> Maybe.map (\col -> { model | sources = col })
382 |> Maybe.withDefault model
383 |> User.saveSources
384
385
386
387-- FORM
388
389
390addSourceUsingForm : Manager
391addSourceUsingForm model =
392 let
393 context =
394 model.sourceForm.context
395
396 cleanContext =
397 { context | data = Dict.map (always String.trim) context.data }
398 in
399 model
400 |> formLens.set Form.initialModel
401 |> addToCollection cleanContext
402 |> andThen returnToIndex
403
404
405editSourceUsingForm : Manager
406editSourceUsingForm model =
407 model
408 |> formLens.set Form.initialModel
409 |> replaceSourceInCollection model.sourceForm.context
410 |> andThen process
411 |> andThen returnToIndex
412
413
414renameSourceUsingForm : Manager
415renameSourceUsingForm model =
416 model
417 |> formLens.set Form.initialModel
418 |> replaceSourceInCollection model.sourceForm.context
419 |> andThen returnToIndex
420
421
422returnToIndex : Manager
423returnToIndex =
424 Common.changeUrlUsingPage (Page.Sources Sources.Index)
425
426
427selectService : String -> Manager
428selectService serviceKey model =
429 case Services.keyToType serviceKey of
430 Just service ->
431 model
432 |> Lens.modify
433 formContextLens
434 (\c ->
435 { c
436 | data = Services.initialData service
437 , service = service
438 }
439 )
440 |> Return.singleton
441
442 Nothing ->
443 Return.singleton model
444
445
446setFormData : String -> String -> Manager
447setFormData key value model =
448 model
449 |> Lens.modify
450 formContextLens
451 (\context ->
452 context.data
453 |> Dict.insert key value
454 |> (\data -> { context | data = data })
455 )
456 |> Return.singleton
457
458
459takeStep : Manager
460takeStep model =
461 let
462 form =
463 formLens.get model
464 in
465 case ( form.step, form.context.service ) of
466 ( How, Dropbox ) ->
467 form.context.data
468 |> Sources.Services.Dropbox.authorizationUrl
469 |> externalAuthorization model
470
471 ( How, Google ) ->
472 form.context.data
473 |> Sources.Services.Google.authorizationUrl
474 |> externalAuthorization model
475
476 _ ->
477 model
478 |> Lens.modify formStepLens takeStepForwards
479 |> Return.singleton
480
481
482takeStepBackwards : Manager
483takeStepBackwards =
484 Lens.modify formStepLens takeStepBackwards_ >> Return.singleton
485
486
487
488-- INDIVIDUAL
489
490
491sourceContextMenu : Source -> Mouse.Event -> Manager
492sourceContextMenu source mouseEvent model =
493 mouseEvent.clientPos
494 |> Coordinates.fromTuple
495 |> Sources.sourceMenu source
496 |> Common.showContextMenuWithModel model
497
498
499toggleActivation : { sourceId : String } -> Manager
500toggleActivation { sourceId } model =
501 model.sources
502 |> List.map
503 (\source ->
504 if source.id == sourceId then
505 { source | enabled = not source.enabled }
506
507 else
508 source
509 )
510 |> (\collection -> { model | sources = collection })
511 |> Tracks.reviseCollection Tracks.Collection.identify
512 |> andThen Common.forceTracksRerender
513 |> andThen Common.generateDirectoryPlaylists
514 |> andThen User.saveSources
515
516
517toggleDirectoryPlaylists : { sourceId : String } -> Manager
518toggleDirectoryPlaylists { sourceId } model =
519 model.sources
520 |> List.map
521 (\source ->
522 if source.id == sourceId then
523 { source | directoryPlaylists = not source.directoryPlaylists }
524
525 else
526 source
527 )
528 |> (\collection -> { model | sources = collection })
529 |> User.saveSources
530 |> andThen Common.generateDirectoryPlaylists
531
532
533toggleProcessAutomatically : Manager
534toggleProcessAutomatically model =
535 User.saveSettings { model | processAutomatically = not model.processAutomatically }
536
537
538
539-- ⚗️
540
541
542externalAuthorization : Model -> (String -> String) -> ( Model, Cmd UI.Msg )
543externalAuthorization model urlBuilder =
544 model.url
545 |> Common.urlOrigin
546 |> urlBuilder
547 |> Nav.load
548 |> return model
549
550
551replaceSourceInCollection : Source -> Manager
552replaceSourceInCollection source model =
553 model.sources
554 |> List.map (\s -> ifThenElse (s.id == source.id) source s)
555 |> (\s -> { model | sources = s })
556 |> User.saveSources
557
558
559sourcesToProcess : Model -> List Source
560sourcesToProcess model =
561 List.filter (.enabled >> (==) True) model.sources
562
563
564takeStepForwards : FormStep -> FormStep
565takeStepForwards currentStep =
566 case currentStep of
567 Where ->
568 How
569
570 _ ->
571 By
572
573
574takeStepBackwards_ : FormStep -> FormStep
575takeStepBackwards_ currentStep =
576 case currentStep of
577 By ->
578 How
579
580 _ ->
581 Where