A music player that connects to your cloud/distributed storage.
at main 15 kB view raw
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