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)