A music player that connects to your cloud/distributed storage.
at main 20 kB view raw
1module UI exposing (main) 2 3import Alien 4import Browser 5import Browser.Events 6import Browser.Navigation as Nav 7import Common exposing (ServiceWorkerStatus(..), Switch(..)) 8import Debouncer.Basic as Debouncer 9import Dict 10import Equalizer 11import InfiniteList 12import Json.Decode as Json 13import Keyboard 14import LastFm 15import Maybe.Extra as Maybe 16import Notifications 17import Random 18import Return 19import Task 20import Time 21import Tracks 22import UI.Adjunct as Adjunct 23import UI.Alfred.State as Alfred 24import UI.Audio.State as Audio 25import UI.Backdrop as Backdrop 26import UI.Common.State as Common 27import UI.DnD as DnD 28import UI.Equalizer.State as Equalizer 29import UI.Interface.State as Interface 30import UI.Other.State as Other 31import UI.Page as Page 32import UI.Playlists.State as Playlists 33import UI.Ports as Ports 34import UI.Queue.State as Queue 35import UI.Queue.Types as Queue 36import UI.Routing.State as Routing 37import UI.Services.State as Services 38import UI.Sources.Form 39import UI.Sources.State as Sources 40import UI.Sources.Types as Sources 41import UI.Syncing.State as Syncing 42import UI.Syncing.Types as Syncing 43import UI.Tracks.State as Tracks 44import UI.Tracks.Types as Tracks 45import UI.Types exposing (..) 46import UI.User.State.Export as User 47import UI.User.State.Import as User 48import UI.View exposing (view) 49import Url exposing (Url) 50import Url.Ext as Url 51 52 53 54-- ⛩ 55 56 57main : Program Flags Model Msg 58main = 59 Browser.application 60 { init = init 61 , view = view 62 , update = update 63 , subscriptions = subscriptions 64 , onUrlChange = UrlChanged 65 , onUrlRequest = LinkClicked 66 } 67 68 69 70-- 🌳 71 72 73init : Flags -> Url -> Nav.Key -> ( Model, Cmd Msg ) 74init flags url key = 75 let 76 rewrittenUrl = 77 Page.rewriteUrl url 78 79 maybePage = 80 Page.fromUrl rewrittenUrl 81 82 page = 83 Maybe.withDefault Page.Index maybePage 84 85 serviceWorkerStatus = 86 if flags.isInstallingServiceWorker then 87 InstallingInitial 88 89 else 90 Activated 91 in 92 { buildTimestamp = flags.buildTimestamp 93 , confirmation = Nothing 94 , currentTime = Time.millisToPosix flags.initialTime 95 , currentTimeZone = Time.utc 96 , darkMode = flags.darkMode 97 , downloading = Nothing 98 , dnd = DnD.initialModel 99 , focusedOnInput = False 100 , isDragging = False 101 , isLoading = True 102 , isOnline = flags.isOnline 103 , isTauri = flags.isTauri 104 , isTouchDevice = False 105 , lastFm = LastFm.initialModel 106 , navKey = key 107 , page = page 108 , pressedKeys = [] 109 , processAutomatically = True 110 , serviceWorkerStatus = serviceWorkerStatus 111 , theme = Nothing 112 , uuidSeed = Random.initialSeed flags.initialTime 113 , url = url 114 , version = flags.version 115 , viewport = flags.viewport 116 117 ----------------------------------------- 118 -- Audio 119 ----------------------------------------- 120 , audioElements = [] 121 , nowPlaying = Nothing 122 , progress = Dict.empty 123 , rememberProgress = True 124 125 ----------------------------------------- 126 -- Backdrop 127 ----------------------------------------- 128 , chosenBackdrop = Nothing 129 , extractedBackdropColor = Nothing 130 , fadeInBackdrop = True 131 , loadedBackdrops = [] 132 133 ----------------------------------------- 134 -- Debouncing 135 ----------------------------------------- 136 , preloadDebouncer = 137 30 138 |> Debouncer.fromSeconds 139 |> Debouncer.debounce 140 |> Debouncer.toDebouncer 141 , progressDebouncer = 142 30 143 |> Debouncer.fromSeconds 144 |> Debouncer.throttle 145 |> Debouncer.emitWhenUnsettled Nothing 146 |> Debouncer.toDebouncer 147 , resizeDebouncer = 148 0.25 149 |> Debouncer.fromSeconds 150 |> Debouncer.debounce 151 |> Debouncer.toDebouncer 152 , searchDebouncer = 153 0.5 154 |> Debouncer.fromSeconds 155 |> Debouncer.debounce 156 |> Debouncer.toDebouncer 157 158 ----------------------------------------- 159 -- Equalizer 160 ----------------------------------------- 161 , eqSettings = Equalizer.defaultSettings 162 , showVolumeSlider = False 163 164 ----------------------------------------- 165 -- Instances 166 ----------------------------------------- 167 , alfred = Nothing 168 , contextMenu = Nothing 169 , notifications = [] 170 171 ----------------------------------------- 172 -- Playlists 173 ----------------------------------------- 174 , editPlaylistContext = Nothing 175 , lastModifiedPlaylist = Nothing 176 , newPlaylistContext = Nothing 177 , playlists = [] 178 , playlistToActivate = Nothing 179 , selectedPlaylist = Nothing 180 181 ----------------------------------------- 182 -- Queue 183 ----------------------------------------- 184 , dontPlay = [] 185 , playedPreviously = [] 186 , playingNext = [] 187 , selectedQueueItem = Nothing 188 189 -- 190 , repeat = False 191 , shuffle = False 192 193 ----------------------------------------- 194 -- Sources 195 ----------------------------------------- 196 , processingContext = [] 197 , processingError = Nothing 198 , processingNotificationId = Nothing 199 , sources = [] 200 , sourceForm = UI.Sources.Form.initialModel 201 202 ----------------------------------------- 203 -- Tracks 204 ----------------------------------------- 205 , cachedCovers = Nothing 206 , cachedTracks = [] 207 , cachedTracksOnly = False 208 , cachingTracksInProgress = [] 209 , covers = { arranged = [], harvested = [] } 210 , coverSelectionReducesPool = True 211 , favourites = [] 212 , favouritesOnly = False 213 , grouping = Nothing 214 , hideDuplicates = False 215 , scene = Tracks.Covers 216 , searchResults = Nothing 217 , searchTerm = Nothing 218 , selectedCover = Nothing 219 , selectedTrackIndexes = [] 220 , sortBy = Tracks.Album 221 , sortDirection = Tracks.Asc 222 , tracks = Tracks.emptyCollection 223 224 -- List scene 225 ------------- 226 , infiniteList = InfiniteList.init 227 228 ----------------------------------------- 229 -- 🦉 Nested 230 ----------------------------------------- 231 , syncing = Syncing.initialModel url 232 } 233 |> Routing.transition 234 page 235 |> Return.command 236 (url 237 |> Syncing.initialCommand 238 |> Cmd.map SyncingMsg 239 ) 240 |> Return.command 241 (if Maybe.isNothing maybePage then 242 Routing.resetUrl key url page 243 244 else 245 case Url.action url of 246 [ "authenticate", "dropbox" ] -> 247 Routing.resetUrl key url page 248 249 _ -> 250 Cmd.none 251 ) 252 |> Return.command 253 (Task.perform SetCurrentTime Time.now) 254 |> Return.command 255 (Task.perform SetCurrentTimeZone Time.here) 256 257 258 259-- 📣 260 261 262update : Msg -> Model -> ( Model, Cmd Msg ) 263update msg = 264 case msg of 265 Bypass -> 266 Return.singleton 267 268 ----------------------------------------- 269 -- Alfred 270 ----------------------------------------- 271 AssignAlfred a -> 272 Alfred.assign a 273 274 GotAlfredInput a -> 275 Alfred.gotInput a 276 277 SelectAlfredItem a -> 278 Alfred.runAction a 279 280 ----------------------------------------- 281 -- Audio 282 ----------------------------------------- 283 AudioDurationChange a -> 284 Audio.durationChange a 285 286 AudioEnded a -> 287 Audio.ended a 288 289 AudioError a -> 290 Audio.error a 291 292 AudioHasLoaded a -> 293 Audio.hasLoaded a 294 295 AudioIsLoading a -> 296 Audio.isLoading a 297 298 AudioPlaybackStateChanged a -> 299 Audio.playbackStateChanged a 300 301 AudioPreloadDebounce a -> 302 Audio.preloadDebounce update a 303 304 AudioTimeUpdated a -> 305 Audio.timeUpdated a 306 307 NoteProgress a -> 308 Audio.noteProgress a 309 310 NoteProgressDebounce a -> 311 Audio.noteProgressDebounce update a 312 313 Pause -> 314 Audio.pause 315 316 Play -> 317 Audio.play 318 319 Seek a -> 320 Audio.seek a 321 322 Stop -> 323 Audio.stop 324 325 TogglePlay -> 326 Audio.playPause 327 328 ToggleRememberProgress -> 329 Audio.toggleRememberProgress 330 331 ----------------------------------------- 332 -- Backdrop 333 ----------------------------------------- 334 ExtractedBackdropColor a -> 335 Backdrop.extractedBackdropColor a 336 337 ChooseBackdrop a -> 338 Backdrop.chooseBackdrop a 339 340 LoadBackdrop a -> 341 Backdrop.loadBackdrop a 342 343 ----------------------------------------- 344 -- Equalizer 345 ----------------------------------------- 346 AdjustVolume a -> 347 Equalizer.adjustVolume a 348 349 ToggleVolumeSlider a -> 350 Equalizer.toggleVolumeSlider a 351 352 ----------------------------------------- 353 -- Interface 354 ----------------------------------------- 355 AssistWithChangingTheme -> 356 Interface.assistWithChangingTheme 357 358 Blur -> 359 Interface.blur 360 361 ChangeTheme a -> 362 Interface.changeTheme a 363 364 ContextMenuConfirmation a b -> 365 Interface.contextMenuConfirmation a b 366 367 CopyToClipboard a -> 368 Interface.copyToClipboard a 369 370 DismissNotification a -> 371 Common.dismissNotification a 372 373 DnD a -> 374 Interface.dnd a 375 376 FocusedOnInput -> 377 Interface.focusedOnInput 378 379 HideOverlay -> 380 Interface.hideOverlay 381 382 LostWindowFocus -> 383 Interface.lostWindowFocus 384 385 MsgViaContextMenu a -> 386 Interface.msgViaContextMenu a 387 388 PreferredColorSchemaChanged a -> 389 Interface.preferredColorSchemaChanged a 390 391 RemoveNotification a -> 392 Interface.removeNotification a 393 394 RemoveQueueSelection -> 395 Interface.removeQueueSelection 396 397 RemoveTrackSelection -> 398 Interface.removeTrackSelection 399 400 ResizeDebounce a -> 401 Interface.resizeDebounce update a 402 403 ResizedWindow a -> 404 Interface.resizedWindow a 405 406 SearchDebounce a -> 407 Interface.searchDebounce update a 408 409 SetIsTouchDevice a -> 410 Interface.setIsTouchDevice a 411 412 ShowNotification a -> 413 Common.showNotification a 414 415 StoppedDragging -> 416 Interface.stoppedDragging 417 418 ToggleLoadingScreen a -> 419 Common.toggleLoadingScreen a 420 421 ----------------------------------------- 422 -- Playlists 423 ----------------------------------------- 424 ActivatePlaylist a -> 425 Playlists.activate a 426 427 AddTracksToPlaylist a -> 428 Playlists.addTracksToPlaylist a 429 430 AssistWithAddingTracksToCollection a -> 431 Playlists.assistWithAddingTracksToCollection a 432 433 AssistWithAddingTracksToPlaylist a -> 434 Playlists.assistWithAddingTracksToPlaylist a 435 436 AssistWithSelectingPlaylist -> 437 Playlists.assistWithSelectingPlaylist 438 439 ConvertCollectionToPlaylist a -> 440 Playlists.convertCollectionToPlaylist a 441 442 ConvertPlaylistToCollection a -> 443 Playlists.convertPlaylistToCollection a 444 445 CreateCollection -> 446 Playlists.createCollection 447 448 CreatePlaylist -> 449 Playlists.createPlaylist 450 451 DeactivatePlaylist -> 452 Playlists.deactivate 453 454 DeletePlaylist a -> 455 Playlists.delete a 456 457 DeselectPlaylist -> 458 Playlists.deselect 459 460 ModifyPlaylist -> 461 Playlists.modify 462 463 MoveTrackInSelectedPlaylist a -> 464 Playlists.moveTrackInSelected a 465 466 RemoveTracksFromPlaylist a b -> 467 Playlists.removeTracks a b 468 469 SelectPlaylist a -> 470 Playlists.select a 471 472 SetPlaylistCreationContext a -> 473 Playlists.setCreationContext a 474 475 SetPlaylistModificationContext a b -> 476 Playlists.setModificationContext a b 477 478 ShowPlaylistListMenu a b -> 479 Playlists.showListMenu a b 480 481 TogglePlaylistVisibility a -> 482 Playlists.toggleVisibility a 483 484 ----------------------------------------- 485 -- Routing 486 ----------------------------------------- 487 ChangeUrlUsingPage a -> 488 Common.changeUrlUsingPage a 489 490 LinkClicked a -> 491 Routing.linkClicked a 492 493 OpenUrlOnNewPage a -> 494 Routing.openUrlOnNewPage a 495 496 PageChanged a -> 497 Routing.transition a 498 499 UrlChanged a -> 500 Routing.urlChanged a 501 502 ----------------------------------------- 503 -- Services 504 ----------------------------------------- 505 ConnectLastFm -> 506 Services.connectLastFm 507 508 DisconnectLastFm -> 509 Services.disconnectLastFm 510 511 GotLastFmSession a -> 512 Services.gotLastFmSession a 513 514 Scrobble a -> 515 Services.scrobble a 516 517 ----------------------------------------- 518 -- User 519 ----------------------------------------- 520 Export -> 521 User.export 522 523 ImportFile a -> 524 User.importFile a 525 526 ImportJson a -> 527 User.importJson a 528 529 InsertDemo -> 530 User.insertDemo 531 532 LoadEnclosedUserData a -> 533 User.loadEnclosedUserData a 534 535 LoadHypaethralUserData a -> 536 User.loadHypaethralUserData a 537 538 RequestImport -> 539 User.requestImport 540 541 SaveEnclosedUserData -> 542 User.saveEnclosedUserData 543 544 ----------------------------------------- 545 -- ⚗️ Adjunct 546 ----------------------------------------- 547 KeyboardMsg a -> 548 Adjunct.keyboardInput a 549 550 ----------------------------------------- 551 -- 🦉 Nested 552 ----------------------------------------- 553 SyncingMsg a -> 554 Syncing.update a 555 556 QueueMsg a -> 557 Queue.update a 558 559 SourcesMsg a -> 560 Sources.update a 561 562 TracksMsg a -> 563 Tracks.update a 564 565 ----------------------------------------- 566 -- 📭 Other 567 ----------------------------------------- 568 InstalledServiceWorker -> 569 Other.installedServiceWorker 570 571 InstallingServiceWorker -> 572 Other.installingServiceWorker 573 574 RedirectToBrain a -> 575 Other.redirectToBrain a 576 577 ReloadApp -> 578 Other.reloadApp 579 580 SetCurrentTime a -> 581 Other.setCurrentTime a 582 583 SetCurrentTimeZone a -> 584 Other.setCurrentTimeZone a 585 586 SetIsOnline a -> 587 Other.setIsOnline a 588 589 590 591-- 📰 592 593 594subscriptions : Model -> Sub Msg 595subscriptions _ = 596 Sub.batch 597 [ Ports.fromAlien alien 598 599 ----------------------------------------- 600 -- Audio 601 ----------------------------------------- 602 , Ports.audioDurationChange AudioDurationChange 603 , Ports.audioEnded AudioEnded 604 , Ports.audioError AudioError 605 , Ports.audioPlaybackStateChanged AudioPlaybackStateChanged 606 , Ports.audioIsLoading AudioIsLoading 607 , Ports.audioHasLoaded AudioHasLoaded 608 , Ports.audioTimeUpdated AudioTimeUpdated 609 , Ports.requestPause (always Pause) 610 , Ports.requestPlay (always Play) 611 , Ports.requestPlayPause (always TogglePlay) 612 , Ports.requestStop (always Stop) 613 614 ----------------------------------------- 615 -- Backdrop 616 ----------------------------------------- 617 , Ports.setAverageBackgroundColor ExtractedBackdropColor 618 619 ----------------------------------------- 620 -- Interface 621 ----------------------------------------- 622 , Browser.Events.onResize Interface.onResize 623 , Ports.indicateTouchDevice (\_ -> SetIsTouchDevice True) 624 , Ports.lostWindowFocus (always LostWindowFocus) 625 , Ports.preferredColorSchemaChanged PreferredColorSchemaChanged 626 , Ports.showErrorNotification (Notifications.error >> ShowNotification) 627 , Ports.showStickyErrorNotification (Notifications.stickyError >> ShowNotification) 628 629 ----------------------------------------- 630 -- Queue 631 ----------------------------------------- 632 , Ports.requestNext (\_ -> QueueMsg Queue.Shift) 633 , Ports.requestPrevious (\_ -> QueueMsg Queue.Rewind) 634 635 ----------------------------------------- 636 -- Services 637 ----------------------------------------- 638 , Ports.scrobble Scrobble 639 640 ----------------------------------------- 641 -- Tracks 642 ----------------------------------------- 643 , Ports.downloadTracksFinished (\_ -> TracksMsg Tracks.DownloadFinished) 644 , Ports.insertCoverCache (TracksMsg << Tracks.InsertCoverCache) 645 646 ----------------------------------------- 647 -- 📭 Other 648 ----------------------------------------- 649 , Ports.installedNewServiceWorker (\_ -> InstalledServiceWorker) 650 , Ports.installingNewServiceWorker (\_ -> InstallingServiceWorker) 651 , Ports.refreshedAccessToken (Alien.broadcast Alien.RefreshedAccessToken >> RedirectToBrain) 652 , Ports.setIsOnline SetIsOnline 653 , Sub.map KeyboardMsg Keyboard.subscriptions 654 , Time.every (60 * 1000) SetCurrentTime 655 ] 656 657 658 659-- 👽 660 661 662alien : Alien.Event -> Msg 663alien event = 664 case ( event.error, Alien.tagFromString event.tag ) of 665 ( Nothing, Just tag ) -> 666 translateAlienData tag event.data 667 668 ( Just err, Just tag ) -> 669 translateAlienError tag event.data err 670 671 _ -> 672 Bypass 673 674 675translateAlienData : Alien.Tag -> Json.Value -> Msg 676translateAlienData tag data = 677 case tag of 678 Alien.AddTracks -> 679 TracksMsg (Tracks.Add data) 680 681 Alien.FinishedProcessingSource -> 682 SourcesMsg (Sources.FinishedProcessingSource data) 683 684 Alien.FinishedProcessingSources -> 685 SourcesMsg Sources.FinishedProcessing 686 687 Alien.GotCachedCover -> 688 TracksMsg (Tracks.GotCachedCover data) 689 690 Alien.HideLoadingScreen -> 691 ToggleLoadingScreen Off 692 693 Alien.LoadEnclosedUserData -> 694 LoadEnclosedUserData data 695 696 Alien.LoadHypaethralUserData -> 697 LoadHypaethralUserData data 698 699 Alien.ReloadTracks -> 700 TracksMsg (Tracks.Reload data) 701 702 Alien.RemoveTracksByPath -> 703 TracksMsg (Tracks.RemoveByPaths data) 704 705 Alien.ReportProcessingError -> 706 SourcesMsg (Sources.ReportProcessingError data) 707 708 Alien.ReportProcessingProgress -> 709 SourcesMsg (Sources.ReportProcessingProgress data) 710 711 Alien.SearchTracks -> 712 TracksMsg (Tracks.SetSearchResults data) 713 714 Alien.StartedSyncing -> 715 SyncingMsg (Syncing.StartedSyncing data) 716 717 Alien.StoreTracksInCache -> 718 TracksMsg (Tracks.StoredInCache data Nothing) 719 720 Alien.SyncMethod -> 721 SyncingMsg (Syncing.GotSyncMethod data) 722 723 Alien.UpdateSourceData -> 724 SourcesMsg (Sources.UpdateSourceData data) 725 726 _ -> 727 Bypass 728 729 730translateAlienError : Alien.Tag -> Json.Value -> String -> Msg 731translateAlienError tag data err = 732 case tag of 733 Alien.StoreTracksInCache -> 734 TracksMsg (Tracks.StoredInCache data <| Just err) 735 736 _ -> 737 if String.startsWith "There seems to be existing data that's encrypted, I will need the passphrase" err then 738 SyncingMsg (Syncing.NeedEncryptionKey { error = err }) 739 740 else 741 ShowNotification (Notifications.stickyError err)