A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Routing and user state

+1871 -1593
+1 -1
.tool-versions
··· 1 1 elm 0.19.1 2 - nodejs 12.13.1 2 + nodejs 13.7.0
+47 -1570
src/Applications/UI.elm
··· 69 69 import UI.Queue as Queue 70 70 import UI.Queue.ContextMenu as Queue 71 71 import UI.Reply as Reply exposing (Reply(..)) 72 + import UI.Reply.Translate as Reply 73 + import UI.Routing.State as Routing 72 74 import UI.Settings as Settings 73 75 import UI.Settings.Page 74 76 import UI.Sources as Sources ··· 79 81 import UI.Tracks as Tracks 80 82 import UI.Tracks.ContextMenu as Tracks 81 83 import UI.Tracks.Scene.List 84 + import UI.Tracks.State as Tracks 82 85 import UI.Types as UI exposing (..) 86 + import UI.User.State as User 87 + import UI.View exposing (view) 83 88 import Url exposing (Protocol(..), Url) 84 89 import Url.Ext as Url 85 90 import User.Layer exposing (..) ··· 149 154 , sources = Sources.initialModel 150 155 , tracks = Tracks.initialModel 151 156 152 - -- Pieces 153 - --------- 157 + -- Parts 158 + -------- 154 159 , audio = Audio.initialModel 155 160 156 161 -- Debouncing ··· 165 170 (PageChanged page) 166 171 |> addCommand 167 172 (if Maybe.isNothing maybePage then 168 - resetUrl key url page 173 + Routing.resetUrl key url page 169 174 170 175 else 171 176 Cmd.none ··· 191 196 translateReply reply model 192 197 193 198 -- 194 - DownloadTracksFinished -> 195 - case model.downloading of 196 - Just { notificationId } -> 197 - { id = notificationId } 198 - |> DismissNotification 199 - |> translateReplyWithModel { model | downloading = Nothing } 200 - 201 - Nothing -> 202 - return model 203 - 204 - LoadEnclosedUserData json -> 205 - model 206 - |> importEnclosed json 207 - |> Return3.wield translateReply 208 - 209 - LoadHypaethralUserData json -> 210 - model 211 - |> importHypaethral json 212 - |> Return3.wield translateReply 213 - |> andThen 214 - (\m -> 215 - case Url.action m.url of 216 - [ "authenticate", "lastfm" ] -> 217 - { authenticating = True 218 - , sessionKey = Nothing 219 - } 220 - |> (\n -> { m | lastFm = n }) 221 - |> returnWithCommand (LastFm.authenticationCommand GotLastFmSession m.url) 222 - 223 - _ -> 224 - return m 225 - ) 226 - |> andThen 227 - (\m -> 228 - if m.isUpgrading then 229 - """ 230 - Thank you for using Diffuse V1! 231 - If you want to import your old data, 232 - please go to the [import page](#/settings/import-export). 233 - """ 234 - |> ShowStickySuccessNotification 235 - |> translateReplyWithModel m 236 - 237 - else 238 - return m 239 - ) 240 - |> andThen 241 - (\m -> 242 - if m.processAutomatically then 243 - m.sources 244 - |> Sources.sourcesToProcess 245 - |> ProcessSources 246 - |> translateReplyWithModel m 199 + Audio a -> 200 + Audio.update a model 247 201 248 - else 249 - return m 250 - ) 202 + Interface a -> 203 + Interface.update a model 251 204 252 205 ----------------------------------------- 253 206 -- Authentication ··· 393 346 } 394 347 395 348 ----------------------------------------- 396 - -- Import / Export 349 + -- Routing 397 350 ----------------------------------------- 398 - Import file -> 399 - 250 400 - |> Process.sleep 401 - |> Task.andThen (\_ -> File.toString file) 402 - |> Task.perform ImportJson 403 - |> returnWithModel { model | isLoading = True } 351 + ChangeUrlUsingPage a -> 352 + Routing.changeUrlUsingPage a model 353 + 354 + LinkClicked a -> 355 + Routing.linkClicked a model 356 + 357 + PageChanged a -> 358 + Routing.transition a model 404 359 405 - ImportJson json -> 406 - json 407 - -- Load data on main thread (this app) 408 - |> Json.Decode.decodeString Json.Decode.value 409 - |> Result.withDefault Json.Encode.null 410 - |> (\j -> importHypaethral j model) 411 - |> Return3.wield translateReply 412 - -- Show notification 413 - |> andThen 414 - ("Imported data successfully!" 415 - |> Notifications.success 416 - |> showNotification 417 - ) 418 - -- Clear tracks cache 419 - |> andThen (translateReply ClearTracksCache) 420 - -- Redirect to index page 421 - |> andThen (update <| ChangeUrlUsingPage Page.Index) 422 - ----------------------------- 423 - -- Save all the imported data 424 - ----------------------------- 425 - |> saveAllHypaethralData 360 + UrlChanged a -> 361 + Routing.urlChanged a model 426 362 427 363 ----------------------------------------- 428 - -- Last.fm 364 + -- Services 429 365 ----------------------------------------- 430 366 GotLastFmSession (Ok sessionKey) -> 431 367 { model | lastFm = LastFm.gotSessionKey sessionKey model.lastFm } ··· 459 395 return model 460 396 461 397 ----------------------------------------- 462 - -- Page Transitions 398 + -- Tracks 463 399 ----------------------------------------- 464 - -- Sources.NewThroughRedirect 465 - ----------------------------- 466 - PageChanged (Page.Sources (UI.Sources.Page.NewThroughRedirect service args)) -> 467 - let 468 - ( sources, form, defaultContext ) = 469 - ( model.sources 470 - , model.sources.form 471 - , UI.Sources.Form.defaultContext 472 - ) 473 - in 474 - { defaultContext 475 - | data = 476 - case service of 477 - Sources.Dropbox -> 478 - Sources.Services.Dropbox.authorizationSourceData args 400 + DownloadTracksFinished -> 401 + case model.downloading of 402 + Just { notificationId } -> 403 + { id = notificationId } 404 + |> DismissNotification 405 + |> Reply.translateWithModel { model | downloading = Nothing } 479 406 480 - Sources.Google -> 481 - Sources.Services.Google.authorizationSourceData args 407 + Nothing -> 408 + return model 482 409 483 - _ -> 484 - defaultContext.data 485 - , service = 486 - service 487 - } 488 - |> (\c -> { form | context = c, step = UI.Sources.Form.By }) 489 - |> (\f -> { sources | form = f }) 490 - |> (\s -> { model | sources = s }) 491 - |> return 492 - 493 - -- Sources.Edit 494 - --------------- 495 - PageChanged (Page.Sources (UI.Sources.Page.Edit sourceId)) -> 496 - loadSourceForForm model sourceId 497 - 498 - -- Sources.Rename 499 - ----------------- 500 - PageChanged (Page.Sources (UI.Sources.Page.Rename sourceId)) -> 501 - loadSourceForForm model sourceId 502 - 503 - -- 504 - PageChanged _ -> 505 - return model 506 - 507 - ----------------------------------------- 508 - -- Tracks Cache 509 - ----------------------------------------- 510 410 FailedToStoreTracksInCache trackIds -> 511 411 model 512 - |> updateTracksModel 412 + |> Lens.modify Tracks.lens 513 413 (\m -> { m | cachingInProgress = List.without trackIds m.cachingInProgress }) 514 414 |> showNotification 515 415 (Notifications.error "Failed to store track in cache") 516 416 517 417 FinishedStoringTracksInCache trackIds -> 518 418 model 519 - |> updateTracksModel 419 + |> Lens.modify Tracks.lens 520 420 (\t -> 521 421 { t 522 422 | cached = t.cached ++ trackIds ··· 542 442 m.tracks.collection.harvested 543 443 |> List.pickIndexes m.tracks.selectedTrackIndexes 544 444 |> ShowTracksContextMenu coordinates { alt = False } 545 - |> translateReplyWithModel m 445 + |> Reply.translateWithModel m 546 446 547 447 else 548 448 return m ··· 554 454 |> andThen (translateReply SaveEnclosedUserData) 555 455 556 456 ----------------------------------------- 557 - -- URL 457 + -- User 558 458 ----------------------------------------- 559 - ChangeUrlUsingPage page -> 560 - page 561 - |> Page.toString 562 - |> Nav.pushUrl model.navKey 563 - |> returnWithModel model 459 + ImportFile a -> 460 + User.importFile a model 564 461 565 - LinkClicked (Browser.Internal urlWithFragment) -> 566 - let 567 - url = 568 - if urlWithFragment.fragment == Just "/" then 569 - { urlWithFragment | fragment = Nothing } 462 + ImportJson a -> 463 + User.importJson a model 570 464 571 - else 572 - urlWithFragment 573 - in 574 - if url.path /= model.url.path then 575 - returnWithModel model (Nav.load url.path) 465 + LoadEnclosedUserData a -> 466 + User.loadEnclosedUserData a model 576 467 577 - else 578 - returnWithModel model (Nav.pushUrl model.navKey <| Url.toString url) 468 + LoadHypaethralUserData a -> 469 + User.loadHypaethralUserData a model 579 470 580 - LinkClicked (Browser.External href) -> 581 - returnWithModel model (Nav.load href) 582 471 583 - UrlChanged url -> 584 - let 585 - rewrittenUrl = 586 - Page.rewriteUrl { url | query = Nothing } 587 - in 588 - case ( url.query, Page.fromUrl rewrittenUrl ) of 589 - ( Nothing, Just page ) -> 590 - { model | page = page, url = url } 591 - |> return 592 - |> andThen (update <| PageChanged page) 593 - 594 - ( Just _, Just page ) -> 595 - returnWithModel model (resetUrl model.navKey url page) 596 - 597 - _ -> 598 - returnWithModel model (resetUrl model.navKey url Page.Index) 599 - 600 - ----------------------------------------- 601 - -- TARGET => Pieces 602 - ----------------------------------------- 603 - Audio a -> 604 - Audio.update a model 605 - 606 - Interface a -> 607 - Interface.update a model 608 - 609 - 610 - updateTracksModel : (Tracks.Model -> Tracks.Model) -> Model -> Model 611 - updateTracksModel fn model = 612 - { model | tracks = fn model.tracks } 613 - 614 - 615 - updateWithModel : Model -> Msg -> Return Model Msg 616 - updateWithModel model msg = 617 - update msg model 618 - 619 - 620 - 621 - -- 📣 ░░ REPLIES 622 - 623 - 624 - translateReply : Reply -> Model -> Return Model Msg 625 - translateReply reply model = 626 - case reply of 627 - Shunt -> 628 - return model 629 - 630 - -- 631 - CopyToClipboard string -> 632 - string 633 - |> Ports.copyToClipboard 634 - |> returnWithModel model 635 - 636 - GoToPage page -> 637 - page 638 - |> ChangeUrlUsingPage 639 - |> updateWithModel model 640 - 641 - StartedDragging -> 642 - return { model | isDragging = True } 643 - 644 - Reply.ToggleLoadingScreen Off -> 645 - return { model | isLoading = False } 646 - 647 - Reply.ToggleLoadingScreen On -> 648 - return { model | isLoading = True } 649 - 650 - ----------------------------------------- 651 - -- Audio 652 - ----------------------------------------- 653 - Seek percentage -> 654 - returnWithModel model (Ports.seek percentage) 655 - 656 - TogglePlayPause -> 657 - Audio.playPause model 658 - 659 - ToggleRememberProgress -> 660 - model 661 - |> Lens.modify 662 - Audio.lens 663 - (\a -> { a | rememberProgress = not a.rememberProgress }) 664 - |> translateReply 665 - SaveSettings 666 - 667 - ----------------------------------------- 668 - -- Authentication 669 - ----------------------------------------- 670 - ExternalAuth Blockstack _ -> 671 - Alien.RedirectToBlockstackSignIn 672 - |> Alien.trigger 673 - |> Ports.toBrain 674 - |> returnWithModel model 675 - 676 - ExternalAuth (Dropbox _) _ -> 677 - [ ( "response_type", "token" ) 678 - , ( "client_id", "te0c9pbeii8f8bw" ) 679 - , ( "redirect_uri", Common.urlOrigin model.url ++ "?action=authenticate/dropbox" ) 680 - ] 681 - |> Common.queryString 682 - |> String.append "https://www.dropbox.com/oauth2/authorize" 683 - |> Nav.load 684 - |> returnWithModel model 685 - 686 - ExternalAuth (RemoteStorage _) input -> 687 - input 688 - |> RemoteStorage.parseUserAddress 689 - |> Maybe.map 690 - (RemoteStorage.webfingerRequest RemoteStorageWebfinger) 691 - |> Maybe.unwrap 692 - (translateReply 693 - (ShowErrorNotification RemoteStorage.userAddressError) 694 - model 695 - ) 696 - (returnWithModel model) 697 - 698 - ExternalAuth _ _ -> 699 - return model 700 - 701 - ImportLegacyData -> 702 - Alien.ImportLegacyData 703 - |> Alien.trigger 704 - |> Ports.toBrain 705 - |> returnWithModel model 706 - |> andThen 707 - (""" 708 - I'll try to import data from Diffuse version one. 709 - If this was successful, you'll get a notification. 710 - """ 711 - |> Notifications.warning 712 - |> showNotification 713 - ) 714 - 715 - PingIpfsForAuth -> 716 - case model.url.protocol of 717 - Https -> 718 - """ 719 - Unfortunately the local IPFS API doesn't work with HTTPS. 720 - Install the [IPFS Companion](https://github.com/ipfs-shipyard/ipfs-companion#release-channel) browser extension to get around this issue 721 - (and make sure it redirects to the local gateway). 722 - """ 723 - |> Notifications.error 724 - |> showNotificationWithModel model 725 - 726 - Http -> 727 - Authentication.PingIpfs 728 - |> AuthenticationMsg 729 - |> updateWithModel model 730 - 731 - PingTextileForAuth -> 732 - Authentication.PingTextile 733 - |> AuthenticationMsg 734 - |> updateWithModel model 735 - 736 - ShowUpdateEncryptionKeyScreen authMethod -> 737 - authMethod 738 - |> Authentication.ShowUpdateEncryptionKeyScreen 739 - |> AuthenticationMsg 740 - |> updateWithModel model 741 - 742 - SignOut -> 743 - let 744 - { playlists, sources, tracks } = 745 - model 746 - in 747 - { model 748 - | authentication = Authentication.Unauthenticated 749 - , playlists = 750 - { playlists 751 - | collection = [] 752 - , playlistToActivate = Maybe.map .name tracks.selectedPlaylist 753 - } 754 - , queue = 755 - Queue.initialModel 756 - , sources = 757 - { sources 758 - | collection = [] 759 - , isProcessing = [] 760 - } 761 - , tracks = 762 - { tracks 763 - | collection = Tracks.emptyCollection 764 - , enabledSourceIds = [] 765 - , favourites = [] 766 - , hideDuplicates = Tracks.initialModel.hideDuplicates 767 - , nowPlaying = Nothing 768 - , searchResults = Nothing 769 - } 770 - } 771 - |> update (BackdropMsg Backdrop.Default) 772 - |> addCommand (Ports.toBrain <| Alien.trigger Alien.SignOut) 773 - |> addCommand (Ports.toBrain <| Alien.trigger Alien.StopProcessing) 774 - |> addCommand (Ports.activeQueueItemChanged Nothing) 775 - |> addCommand (Nav.pushUrl model.navKey "#/") 776 - 777 - ----------------------------------------- 778 - -- Context Menu 779 - ----------------------------------------- 780 - ContextMenuConfirmation conf r -> 781 - { model | confirmation = Just conf } 782 - |> return 783 - |> andThen (translateReply r) 784 - 785 - ReplyViaContextMenu r -> 786 - case r of 787 - ContextMenuConfirmation _ _ -> 788 - translateReply r model 789 - 790 - _ -> 791 - translateReply r { model | contextMenu = Nothing } 792 - 793 - ShowMoreAuthenticationOptions coordinates -> 794 - return { model | contextMenu = Just (Authentication.moreOptionsMenu coordinates) } 795 - 796 - ShowPlaylistListMenu coordinates playlist -> 797 - return { model | contextMenu = Just (Playlists.listMenu playlist model.tracks.collection.identified model.confirmation coordinates) } 798 - 799 - ShowQueueFutureMenu coordinates { item, itemIndex } -> 800 - return { model | contextMenu = Just (Queue.futureMenu { cached = model.tracks.cached, cachingInProgress = model.tracks.cachingInProgress, itemIndex = itemIndex } item coordinates) } 801 - 802 - ShowQueueHistoryMenu coordinates { item } -> 803 - return { model | contextMenu = Just (Queue.historyMenu { cached = model.tracks.cached, cachingInProgress = model.tracks.cachingInProgress } item coordinates) } 804 - 805 - ShowSourceContextMenu coordinates source -> 806 - return { model | contextMenu = Just (Sources.sourceMenu source coordinates) } 807 - 808 - ShowTracksContextMenu coordinates { alt } tracks -> 809 - let 810 - menuDependencies = 811 - { cached = model.tracks.cached 812 - , cachingInProgress = model.tracks.cachingInProgress 813 - , currentTime = model.currentTime 814 - , selectedPlaylist = model.tracks.selectedPlaylist 815 - , lastModifiedPlaylistName = model.playlists.lastModifiedPlaylist 816 - , showAlternativeMenu = alt 817 - , sources = model.sources.collection 818 - } 819 - in 820 - return { model | contextMenu = Just (Tracks.trackMenu menuDependencies tracks coordinates) } 821 - 822 - ShowTracksViewMenu coordinates maybeGrouping -> 823 - return { model | contextMenu = Just (Tracks.viewMenu model.tracks.cachedOnly maybeGrouping coordinates) } 824 - 825 - ----------------------------------------- 826 - -- Last.fm 827 - ----------------------------------------- 828 - ConnectLastFm -> 829 - model.url 830 - |> Common.urlOrigin 831 - |> String.addSuffix "?action=authenticate/lastfm" 832 - |> Url.percentEncode 833 - |> String.append "&cb=" 834 - |> String.append 835 - (String.append 836 - "http://www.last.fm/api/auth/?api_key=" 837 - LastFm.apiKey 838 - ) 839 - |> Nav.load 840 - |> returnWithModel model 841 - 842 - DisconnectLastFm -> 843 - translateReply 844 - SaveSettings 845 - { model | lastFm = LastFm.disconnect model.lastFm } 846 - 847 - ----------------------------------------- 848 - -- Notifications 849 - ----------------------------------------- 850 - DismissNotification options -> 851 - options 852 - |> UI.Notifications.dismiss model.notifications 853 - |> mapModel (\n -> { model | notifications = n }) 854 - |> mapCommand Reply 855 - 856 - RemoveNotification { id } -> 857 - model.notifications 858 - |> List.filter (Notifications.id >> (/=) id) 859 - |> (\n -> { model | notifications = n }) 860 - |> return 861 - 862 - ShowErrorNotification string -> 863 - showNotificationWithModel model (Notifications.error string) 864 - 865 - ShowStickyErrorNotification string -> 866 - showNotificationWithModel model (Notifications.stickyError string) 867 - 868 - ShowStickyErrorNotificationWithCode string code -> 869 - showNotificationWithModel model (Notifications.errorWithCode string code []) 870 - 871 - ShowSuccessNotification string -> 872 - showNotificationWithModel model (Notifications.success string) 873 - 874 - ShowStickySuccessNotification string -> 875 - showNotificationWithModel model (Notifications.stickySuccess string) 876 - 877 - ShowWarningNotification string -> 878 - showNotificationWithModel model (Notifications.warning string) 879 - 880 - ShowStickyWarningNotification string -> 881 - showNotificationWithModel model (Notifications.stickyWarning string) 882 - 883 - ----------------------------------------- 884 - -- Playlists 885 - ----------------------------------------- 886 - ActivatePlaylist playlist -> 887 - playlist 888 - |> Tracks.SelectPlaylist 889 - |> TracksMsg 890 - |> updateWithModel model 891 - 892 - AddTracksToPlaylist { playlistName, tracks } -> 893 - let 894 - properPlaylistName = 895 - String.trim playlistName 896 - 897 - playlistIndex = 898 - List.findIndex 899 - (\p -> p.autoGenerated == False && p.name == properPlaylistName) 900 - model.playlists.collection 901 - 902 - playlistsModel = 903 - model.playlists 904 - 905 - newCollection = 906 - case playlistIndex of 907 - Just idx -> 908 - List.updateAt 909 - idx 910 - (\p -> { p | tracks = p.tracks ++ tracks }) 911 - playlistsModel.collection 912 - 913 - Nothing -> 914 - (::) 915 - { autoGenerated = False 916 - , name = properPlaylistName 917 - , tracks = tracks 918 - } 919 - playlistsModel.collection 920 - 921 - newModel = 922 - { playlistsModel 923 - | collection = newCollection 924 - , lastModifiedPlaylist = Just properPlaylistName 925 - } 926 - |> (\m -> { model | playlists = m }) 927 - in 928 - (case tracks of 929 - [ t ] -> 930 - "Added __" ++ t.title ++ "__" 931 - 932 - l -> 933 - "Added __" ++ String.fromInt (List.length l) ++ " tracks__" 934 - ) 935 - |> (\s -> s ++ " to the __" ++ properPlaylistName ++ "__ playlist") 936 - |> Notifications.success 937 - |> showNotificationWithModel newModel 938 - |> andThen (translateReply SavePlaylists) 939 - 940 - DeactivatePlaylist -> 941 - Tracks.DeselectPlaylist 942 - |> TracksMsg 943 - |> updateWithModel model 944 - 945 - GenerateDirectoryPlaylists -> 946 - let 947 - nonDirectoryPlaylists = 948 - List.filterNot 949 - .autoGenerated 950 - model.playlists.collection 951 - 952 - directoryPlaylists = 953 - UI.Playlists.Directory.generate 954 - model.sources.collection 955 - model.tracks.collection.untouched 956 - 957 - playlists = 958 - model.playlists 959 - in 960 - [ nonDirectoryPlaylists 961 - , directoryPlaylists 962 - ] 963 - |> List.concat 964 - |> (\c -> { playlists | collection = c }) 965 - |> (\p -> { model | playlists = p }) 966 - |> return 967 - 968 - RemoveFromSelectedPlaylist playlist tracks -> 969 - let 970 - updatedPlaylist = 971 - Tracks.removeFromPlaylist tracks playlist 972 - 973 - ( tracksModel, playlistsModel ) = 974 - ( model.tracks 975 - , model.playlists 976 - ) 977 - 978 - newPlaylistsCollection = 979 - List.map 980 - (\p -> 981 - if p.name == playlist.name then 982 - updatedPlaylist 983 - 984 - else 985 - p 986 - ) 987 - model.playlists.collection 988 - in 989 - newPlaylistsCollection 990 - |> (\c -> { playlistsModel | collection = c }) 991 - |> (\p -> { model | playlists = p }) 992 - |> update (TracksMsg <| Tracks.SelectPlaylist updatedPlaylist) 993 - |> andThen (translateReply SavePlaylists) 994 - 995 - RemovePlaylistFromCollection args -> 996 - args 997 - |> Playlists.RemoveFromCollection 998 - |> PlaylistsMsg 999 - |> updateWithModel { model | confirmation = Nothing } 1000 - 1001 - ReplacePlaylistInCollection playlist -> 1002 - let 1003 - playlists = 1004 - model.playlists 1005 - in 1006 - playlists.collection 1007 - |> List.map (\p -> ifThenElse (p.name == playlist.name) playlist p) 1008 - |> (\c -> { playlists | collection = c }) 1009 - |> (\p -> { model | playlists = p }) 1010 - |> translateReply SavePlaylists 1011 - 1012 - RequestAssistanceForPlaylists tracks -> 1013 - model.playlists.collection 1014 - |> List.filterNot .autoGenerated 1015 - |> UI.Playlists.Alfred.create tracks 1016 - |> Alfred.Assign 1017 - |> AlfredMsg 1018 - |> updateWithModel model 1019 - 1020 - ----------------------------------------- 1021 - -- Queue 1022 - ----------------------------------------- 1023 - ActiveQueueItemChanged maybeQueueItem -> 1024 - let 1025 - nowPlaying = 1026 - Maybe.map .identifiedTrack maybeQueueItem 1027 - 1028 - portCmd = 1029 - maybeQueueItem 1030 - |> Maybe.map 1031 - (.identifiedTrack >> Tuple.second) 1032 - |> Maybe.map 1033 - (Queue.makeEngineItem 1034 - model.currentTime 1035 - model.sources.collection 1036 - model.tracks.cached 1037 - (if model.audio.rememberProgress then 1038 - model.audio.progress 1039 - 1040 - else 1041 - Dict.empty 1042 - ) 1043 - ) 1044 - |> Ports.activeQueueItemChanged 1045 - in 1046 - model 1047 - |> update (TracksMsg <| Tracks.SetNowPlaying nowPlaying) 1048 - |> addCommand portCmd 1049 - 1050 - AddToQueue { inFront, tracks } -> 1051 - (if inFront then 1052 - Queue.InjectFirst 1053 - 1054 - else 1055 - Queue.InjectLast 1056 - ) 1057 - |> (\msg -> msg { showNotification = True } tracks) 1058 - |> QueueMsg 1059 - |> updateWithModel model 1060 - 1061 - FillQueue -> 1062 - model.tracks.collection.harvested 1063 - |> Queue.Fill model.currentTime 1064 - |> QueueMsg 1065 - |> updateWithModel model 1066 - 1067 - MoveQueueItemToFirst args -> 1068 - translateReply 1069 - FillQueue 1070 - { model | queue = Queue.moveQueueItemToFirst model.queue args } 1071 - 1072 - MoveQueueItemToLast args -> 1073 - translateReply 1074 - FillQueue 1075 - { model | queue = Queue.moveQueueItemToLast model.queue args } 1076 - 1077 - PlayTrack identifiedTrack -> 1078 - identifiedTrack 1079 - |> Queue.InjectFirstAndPlay 1080 - |> QueueMsg 1081 - |> updateWithModel model 1082 - 1083 - ResetQueue -> 1084 - update (QueueMsg Queue.Reset) model 1085 - 1086 - RewindQueue -> 1087 - update (QueueMsg Queue.Rewind) model 1088 - 1089 - ShiftQueue -> 1090 - update (QueueMsg Queue.Shift) model 1091 - 1092 - ToggleRepeat -> 1093 - update (QueueMsg Queue.ToggleRepeat) model 1094 - 1095 - ToggleShuffle -> 1096 - update (QueueMsg Queue.ToggleShuffle) model 1097 - 1098 - ----------------------------------------- 1099 - -- Sources & Tracks 1100 - ----------------------------------------- 1101 - AddSourceToCollection source -> 1102 - source 1103 - |> Sources.AddToCollection 1104 - |> SourcesMsg 1105 - |> updateWithModel model 1106 - 1107 - ClearTracksCache -> 1108 - model.tracks.cached 1109 - |> Json.Encode.list Json.Encode.string 1110 - |> Alien.broadcast Alien.RemoveTracksFromCache 1111 - |> Ports.toBrain 1112 - |> returnWithModel (updateTracksModel (\m -> { m | cached = [] }) model) 1113 - |> andThen (update <| TracksMsg Tracks.Harvest) 1114 - |> andThen (translateReply <| SaveEnclosedUserData) 1115 - |> andThen (translateReply <| ShowWarningNotification "Tracks cache was cleared") 1116 - 1117 - DisableTracksGrouping -> 1118 - Tracks.DisableGrouping 1119 - |> TracksMsg 1120 - |> updateWithModel model 1121 - 1122 - DownloadTracks zipName tracks -> 1123 - let 1124 - notification = 1125 - Notifications.stickyWarning "Downloading tracks ..." 1126 - 1127 - downloading = 1128 - Just { notificationId = Notifications.id notification } 1129 - in 1130 - [ ( "zipName", Json.Encode.string zipName ) 1131 - , ( "trackIds" 1132 - , tracks 1133 - |> List.map .id 1134 - |> Json.Encode.list Json.Encode.string 1135 - ) 1136 - ] 1137 - |> Json.Encode.object 1138 - |> Alien.broadcast Alien.DownloadTracks 1139 - |> Ports.toBrain 1140 - |> returnWithModel { model | downloading = downloading } 1141 - |> andThen (showNotification notification) 1142 - 1143 - ExternalSourceAuthorization urlBuilder -> 1144 - model.url 1145 - |> Common.urlOrigin 1146 - |> urlBuilder 1147 - |> Nav.load 1148 - |> returnWithModel model 1149 - 1150 - ForceTracksRerender -> 1151 - ( model 1152 - , Task.attempt 1153 - (always Bypass) 1154 - (Browser.Dom.setViewportOf UI.Tracks.Scene.List.containerId 0 1) 1155 - ) 1156 - 1157 - GroupTracksBy grouping -> 1158 - grouping 1159 - |> Tracks.GroupBy 1160 - |> TracksMsg 1161 - |> updateWithModel model 1162 - 1163 - PreloadNextTrack -> 1164 - case List.head model.queue.future of 1165 - Just item -> 1166 - item 1167 - |> .identifiedTrack 1168 - |> Tuple.second 1169 - |> Queue.makeEngineItem 1170 - model.currentTime 1171 - model.sources.collection 1172 - model.tracks.cached 1173 - (if model.audio.rememberProgress then 1174 - model.audio.progress 1175 - 1176 - else 1177 - Dict.empty 1178 - ) 1179 - |> Ports.preloadAudio 1180 - |> returnWithModel model 1181 - 1182 - Nothing -> 1183 - return model 1184 - 1185 - ProcessSources [] -> 1186 - return model 1187 - 1188 - ProcessSources sourcesToProcess -> 1189 - let 1190 - notification = 1191 - Notifications.stickyWarning "Processing sources ..." 1192 - 1193 - notificationId = 1194 - Notifications.id notification 1195 - 1196 - newNotifications = 1197 - List.filter 1198 - (\n -> Notifications.kind n /= Notifications.Error) 1199 - model.notifications 1200 - 1201 - sources = 1202 - model.sources 1203 - 1204 - isProcessing = 1205 - sourcesToProcess 1206 - |> List.sortBy (.data >> Dict.fetch "name" "") 1207 - |> List.map (\{ id } -> ( id, 0 )) 1208 - 1209 - newSources = 1210 - { sources 1211 - | isProcessing = isProcessing 1212 - , processingError = Nothing 1213 - , processingNotificationId = Just notificationId 1214 - } 1215 - 1216 - newModel = 1217 - { model | notifications = newNotifications, sources = newSources } 1218 - in 1219 - [ ( "origin" 1220 - , Json.Encode.string (Common.urlOrigin model.url) 1221 - ) 1222 - , ( "sources" 1223 - , Json.Encode.list Sources.encode sourcesToProcess 1224 - ) 1225 - ] 1226 - |> Json.Encode.object 1227 - |> Alien.broadcast Alien.ProcessSources 1228 - |> Ports.toBrain 1229 - |> returnWithModel newModel 1230 - |> andThen (showNotification notification) 1231 - 1232 - RemoveSourceFromCollection args -> 1233 - args 1234 - |> Sources.RemoveFromCollection 1235 - |> SourcesMsg 1236 - |> updateWithModel model 1237 - 1238 - RemoveTracksFromCache tracks -> 1239 - let 1240 - trackIds = 1241 - List.map .id tracks 1242 - in 1243 - trackIds 1244 - |> Json.Encode.list Json.Encode.string 1245 - |> Alien.broadcast Alien.RemoveTracksFromCache 1246 - |> Ports.toBrain 1247 - |> returnWithModel 1248 - (updateTracksModel 1249 - (\m -> { m | cached = List.without trackIds m.cached }) 1250 - model 1251 - ) 1252 - |> andThen (update <| TracksMsg Tracks.Harvest) 1253 - |> andThen (translateReply SaveEnclosedUserData) 1254 - 1255 - RemoveTracksWithSourceId sourceId -> 1256 - let 1257 - cmd = 1258 - sourceId 1259 - |> Json.Encode.string 1260 - |> Alien.broadcast Alien.RemoveTracksBySourceId 1261 - |> Ports.toBrain 1262 - in 1263 - sourceId 1264 - |> Tracks.RemoveBySourceId 1265 - |> TracksMsg 1266 - |> updateWithModel model 1267 - |> addCommand cmd 1268 - 1269 - ReplaceSourceInCollection source -> 1270 - let 1271 - sources = 1272 - model.sources 1273 - in 1274 - model.sources.collection 1275 - |> List.map (\s -> ifThenElse (s.id == source.id) source s) 1276 - |> (\c -> { sources | collection = c }) 1277 - |> (\s -> { model | sources = s }) 1278 - |> return 1279 - |> andThen (translateReply SaveSources) 1280 - 1281 - ScrollToNowPlaying -> 1282 - update (TracksMsg Tracks.ScrollToNowPlaying) model 1283 - 1284 - StoreTracksInCache tracks -> 1285 - let 1286 - trackIds = 1287 - List.map .id tracks 1288 - 1289 - notification = 1290 - case tracks of 1291 - [ t ] -> 1292 - ("__" ++ t.tags.title ++ "__ will be stored in the cache") 1293 - |> Notifications.success 1294 - 1295 - list -> 1296 - list 1297 - |> List.length 1298 - |> String.fromInt 1299 - |> (\s -> "__" ++ s ++ " tracks__ will be stored in the cache") 1300 - |> Notifications.success 1301 - in 1302 - tracks 1303 - |> Json.Encode.list 1304 - (\track -> 1305 - Json.Encode.object 1306 - [ ( "trackId" 1307 - , Json.Encode.string track.id 1308 - ) 1309 - , ( "url" 1310 - , track 1311 - |> Queue.makeTrackUrl 1312 - model.currentTime 1313 - model.sources.collection 1314 - |> Json.Encode.string 1315 - ) 1316 - ] 1317 - ) 1318 - |> Alien.broadcast Alien.StoreTracksInCache 1319 - |> Ports.toBrain 1320 - |> returnWithModel 1321 - (updateTracksModel 1322 - (\m -> { m | cachingInProgress = m.cachingInProgress ++ trackIds }) 1323 - model 1324 - ) 1325 - |> andThen (showNotification notification) 1326 - 1327 - ToggleCachedTracksOnly -> 1328 - update (TracksMsg Tracks.ToggleCachedOnly) model 1329 - 1330 - ToggleDirectoryPlaylists args -> 1331 - update (SourcesMsg <| Sources.ToggleDirectoryPlaylists args) model 1332 - 1333 - ToggleHideDuplicates -> 1334 - update (TracksMsg Tracks.ToggleHideDuplicates) model 1335 - 1336 - ToggleProcessAutomatically -> 1337 - translateReply SaveSettings { model | processAutomatically = not model.processAutomatically } 1338 - 1339 - ----------------------------------------- 1340 - -- User Data 1341 - ----------------------------------------- 1342 - ChooseBackdrop filename -> 1343 - filename 1344 - |> Backdrop.Choose 1345 - |> BackdropMsg 1346 - |> updateWithModel model 1347 - 1348 - Export -> 1349 - { favourites = model.tracks.favourites 1350 - , playlists = List.filterNot .autoGenerated model.playlists.collection 1351 - , progress = model.audio.progress 1352 - , settings = Just (gatherSettings model) 1353 - , sources = model.sources.collection 1354 - , tracks = model.tracks.collection.untouched 1355 - } 1356 - |> encodeHypaethralData 1357 - |> Json.Encode.encode 2 1358 - |> File.Download.string "diffuse.json" "application/json" 1359 - |> returnWithModel model 1360 - 1361 - InsertDemo -> 1362 - model.currentTime 1363 - |> Demo.tape 1364 - |> LoadHypaethralUserData 1365 - |> updateWithModel model 1366 - |> saveAllHypaethralData 1367 - 1368 - LoadDefaultBackdrop -> 1369 - Backdrop.Default 1370 - |> BackdropMsg 1371 - |> updateWithModel model 1372 - 1373 - RequestImport -> 1374 - Import 1375 - |> File.Select.file [ "application/json" ] 1376 - |> returnWithModel model 1377 - 1378 - SaveEnclosedUserData -> 1379 - model 1380 - |> exportEnclosed 1381 - |> Alien.broadcast Alien.SaveEnclosedUserData 1382 - |> Ports.toBrain 1383 - |> returnWithModel model 1384 - 1385 - SaveFavourites -> 1386 - model.tracks.favourites 1387 - |> Json.Encode.list Tracks.encodeFavourite 1388 - |> Alien.broadcast Alien.SaveFavourites 1389 - |> Ports.toBrain 1390 - |> returnWithModel model 1391 - 1392 - SavePlaylists -> 1393 - model.playlists.collection 1394 - |> List.filterNot .autoGenerated 1395 - |> Json.Encode.list Playlists.encode 1396 - |> Alien.broadcast Alien.SavePlaylists 1397 - |> Ports.toBrain 1398 - |> returnWithModel model 1399 - 1400 - SaveProgress -> 1401 - model.audio.progress 1402 - |> Json.Encode.dict identity Json.Encode.float 1403 - |> Alien.broadcast Alien.SaveProgress 1404 - |> Ports.toBrain 1405 - |> returnWithModel model 1406 - 1407 - SaveSettings -> 1408 - model 1409 - |> gatherSettings 1410 - |> Settings.encode 1411 - |> Alien.broadcast Alien.SaveSettings 1412 - |> Ports.toBrain 1413 - |> returnWithModel model 1414 - 1415 - SaveSources -> 1416 - let 1417 - updateEnabledSourceIdsOnTracks = 1418 - model.sources.collection 1419 - |> Sources.enabledSourceIds 1420 - |> Tracks.SetEnabledSourceIds 1421 - |> TracksMsg 1422 - |> update 1423 - 1424 - ( updatedModel, updatedCmd ) = 1425 - updateEnabledSourceIdsOnTracks model 1426 - in 1427 - updatedModel.sources.collection 1428 - |> Json.Encode.list Sources.encode 1429 - |> Alien.broadcast Alien.SaveSources 1430 - |> Ports.toBrain 1431 - |> returnWithModel updatedModel 1432 - |> addCommand updatedCmd 1433 - 1434 - SaveTracks -> 1435 - model.tracks.collection.untouched 1436 - |> Json.Encode.list Tracks.encodeTrack 1437 - |> Alien.broadcast Alien.SaveTracks 1438 - |> Ports.toBrain 1439 - |> returnWithModel model 1440 - 1441 - 1442 - translateReplyWithModel : Model -> Reply -> Return Model Msg 1443 - translateReplyWithModel model reply = 1444 - translateReply reply model 1445 - 1446 - 1447 - 1448 - -- 📣 ░░ FUNCTIONS 1449 - 1450 - 1451 - loadSourceForForm : Model -> String -> ( Model, Cmd Msg ) 1452 - loadSourceForForm model sourceId = 1453 - let 1454 - isLoading = 1455 - model.isLoading 1456 - 1457 - maybeSource = 1458 - List.find (.id >> (==) sourceId) model.sources.collection 1459 - in 1460 - case ( isLoading, maybeSource ) of 1461 - ( False, Just source ) -> 1462 - let 1463 - ( sources, form ) = 1464 - ( model.sources 1465 - , model.sources.form 1466 - ) 1467 - 1468 - newForm = 1469 - { form | context = source } 1470 - 1471 - newSources = 1472 - { sources | form = newForm } 1473 - in 1474 - return { model | sources = newSources } 1475 - 1476 - ( False, Nothing ) -> 1477 - return model 1478 - 1479 - ( True, _ ) -> 1480 - -- Redirect away from edit-source page 1481 - UI.Sources.Page.Index 1482 - |> Page.Sources 1483 - |> ChangeUrlUsingPage 1484 - |> updateWithModel model 1485 - 1486 - 1487 - resetUrl : Nav.Key -> Url -> Page.Page -> Cmd Msg 1488 - resetUrl key url page = 1489 - Nav.replaceUrl key (url.path ++ Page.toString page) 1490 - 1491 - 1492 - saveAllHypaethralData : Return Model Msg -> Return Model Msg 1493 - saveAllHypaethralData return = 1494 - List.foldl 1495 - (\( _, bit ) -> 1496 - case bit of 1497 - Favourites -> 1498 - andThen (translateReply SaveFavourites) 1499 - 1500 - Playlists -> 1501 - andThen (translateReply SavePlaylists) 1502 - 1503 - Progress -> 1504 - andThen (translateReply SaveProgress) 1505 - 1506 - Settings -> 1507 - andThen (translateReply SaveSettings) 1508 - 1509 - Sources -> 1510 - andThen (translateReply SaveSources) 1511 - 1512 - Tracks -> 1513 - andThen (translateReply SaveTracks) 1514 - ) 1515 - return 1516 - hypaethralBit.list 472 + translateReply = 473 + Reply.translate 1517 474 1518 475 1519 476 ··· 1685 642 |> Notifications.error 1686 643 |> Interface.ShowNotification 1687 644 |> Interface 1688 - 1689 - 1690 - 1691 - -- 🗺 1692 - 1693 - 1694 - view : Model -> Browser.Document Msg 1695 - view model = 1696 - { title = "Diffuse" 1697 - , body = [ body model ] 1698 - } 1699 - 1700 - 1701 - body : Model -> Html Msg 1702 - body model = 1703 - section 1704 - (if Maybe.isJust model.contextMenu || Maybe.isJust model.alfred.instance then 1705 - [ on "tap" (interfaceEventHandler Interface.HideOverlay) ] 1706 - 1707 - else if Maybe.isJust model.equalizer.activeKnob then 1708 - [ Pointer.onMove (EqualizerMsg << Equalizer.AdjustKnob) 1709 - , Pointer.onUp (EqualizerMsg << Equalizer.DeactivateKnob) 1710 - , Pointer.onCancel (EqualizerMsg << Equalizer.DeactivateKnob) 1711 - ] 1712 - 1713 - else if model.isDragging then 1714 - [ class C.dragging_something 1715 - , on "mouseup" (interfaceEventHandler Interface.StoppedDragging) 1716 - , on "touchcancel" (interfaceEventHandler Interface.StoppedDragging) 1717 - , on "touchend" (interfaceEventHandler Interface.StoppedDragging) 1718 - ] 1719 - 1720 - else if Maybe.isJust model.queue.selection then 1721 - [ on "tap" (interfaceEventHandler Interface.RemoveQueueSelection) ] 1722 - 1723 - else if not (List.isEmpty model.tracks.selectedTrackIndexes) then 1724 - [ on "tap" (interfaceEventHandler Interface.RemoveTrackSelection) ] 1725 - 1726 - else 1727 - [] 1728 - ) 1729 - [ ----------------------------------------- 1730 - -- Alfred 1731 - ----------------------------------------- 1732 - model.alfred 1733 - |> Lazy.lazy Alfred.view 1734 - |> Html.map AlfredMsg 1735 - 1736 - ----------------------------------------- 1737 - -- Backdrop 1738 - ----------------------------------------- 1739 - , model.backdrop 1740 - |> Lazy.lazy Backdrop.view 1741 - |> Html.map BackdropMsg 1742 - 1743 - ----------------------------------------- 1744 - -- Context Menu 1745 - ----------------------------------------- 1746 - , model.contextMenu 1747 - |> Lazy.lazy UI.ContextMenu.view 1748 - |> Html.map Reply 1749 - 1750 - ----------------------------------------- 1751 - -- Notifications 1752 - ----------------------------------------- 1753 - , model.notifications 1754 - |> Lazy.lazy UI.Notifications.view 1755 - |> Html.map Reply 1756 - 1757 - ----------------------------------------- 1758 - -- Overlay 1759 - ----------------------------------------- 1760 - , model.contextMenu 1761 - |> Lazy.lazy2 overlay model.alfred.instance 1762 - 1763 - ----------------------------------------- 1764 - -- Content 1765 - ----------------------------------------- 1766 - , let 1767 - opts = 1768 - { justifyCenter = False 1769 - , scrolling = not model.isDragging 1770 - } 1771 - in 1772 - case ( model.isLoading, model.authentication ) of 1773 - ( True, _ ) -> 1774 - content { opts | justifyCenter = True } [ loadingAnimation ] 1775 - 1776 - ( False, Authentication.Authenticated _ ) -> 1777 - content opts (defaultScreen model) 1778 - 1779 - ( False, _ ) -> 1780 - model.authentication 1781 - |> Lazy.lazy Authentication.view 1782 - |> Html.map AuthenticationMsg 1783 - |> List.singleton 1784 - |> content opts 1785 - ] 1786 - 1787 - 1788 - defaultScreen : Model -> List (Html Msg) 1789 - defaultScreen model = 1790 - [ Lazy.lazy2 1791 - (Navigation.global 1792 - [ ( Page.Index, "Tracks" ) 1793 - , ( Page.Sources UI.Sources.Page.Index, "Sources" ) 1794 - , ( Page.Settings UI.Settings.Page.Index, "Settings" ) 1795 - ] 1796 - ) 1797 - model.alfred.instance 1798 - model.page 1799 - 1800 - ----------------------------------------- 1801 - -- Main 1802 - ----------------------------------------- 1803 - , vessel 1804 - [ { amountOfSources = List.length model.sources.collection 1805 - , bgColor = model.backdrop.bgColor 1806 - , darkMode = model.darkMode 1807 - , isOnIndexPage = model.page == Page.Index 1808 - , isTouchDevice = model.isTouchDevice 1809 - , sourceIdsBeingProcessed = List.map Tuple.first model.sources.isProcessing 1810 - , viewport = model.viewport 1811 - } 1812 - |> Tracks.view model.tracks 1813 - |> Html.map TracksMsg 1814 - 1815 - -- Pages 1816 - -------- 1817 - , case model.page of 1818 - Page.Equalizer -> 1819 - model.equalizer 1820 - |> Lazy.lazy Equalizer.view 1821 - |> Html.map EqualizerMsg 1822 - 1823 - Page.Index -> 1824 - nothing 1825 - 1826 - Page.Playlists subPage -> 1827 - model.backdrop.bgColor 1828 - |> Lazy.lazy4 1829 - Playlists.view 1830 - subPage 1831 - model.playlists 1832 - model.tracks.selectedPlaylist 1833 - |> Html.map PlaylistsMsg 1834 - 1835 - Page.Queue subPage -> 1836 - model.queue 1837 - |> Lazy.lazy2 Queue.view subPage 1838 - |> Html.map QueueMsg 1839 - 1840 - Page.Settings subPage -> 1841 - { authenticationMethod = Authentication.extractMethod model.authentication 1842 - , chosenBackgroundImage = model.backdrop.chosen 1843 - , hideDuplicateTracks = model.tracks.hideDuplicates 1844 - , lastFm = model.lastFm 1845 - , processAutomatically = model.processAutomatically 1846 - , rememberProgress = model.audio.rememberProgress 1847 - } 1848 - |> Lazy.lazy2 Settings.view subPage 1849 - |> Html.map Reply 1850 - 1851 - Page.Sources subPage -> 1852 - let 1853 - amountOfTracks = 1854 - List.length model.tracks.collection.untouched 1855 - in 1856 - model.sources 1857 - |> Lazy.lazy3 Sources.view { amountOfTracks = amountOfTracks } subPage 1858 - |> Html.map SourcesMsg 1859 - ] 1860 - 1861 - ----------------------------------------- 1862 - -- Controls 1863 - ----------------------------------------- 1864 - , Html.map Reply 1865 - (UI.Console.view 1866 - model.queue.activeItem 1867 - model.queue.repeat 1868 - model.queue.shuffle 1869 - { stalled = model.audio.hasStalled 1870 - , loading = model.audio.isLoading 1871 - , playing = model.audio.isPlaying 1872 - } 1873 - ( model.audio.position 1874 - , model.audio.duration 1875 - ) 1876 - ) 1877 - ] 1878 - 1879 - 1880 - 1881 - -- 🗺 ░░ BITS 1882 - 1883 - 1884 - content : { justifyCenter : Bool, scrolling : Bool } -> List (Html Msg) -> Html Msg 1885 - content { justifyCenter, scrolling } nodes = 1886 - brick 1887 - [ on "focusout" (interfaceEventHandler Interface.Blur) 1888 - , on "focusin" inputFocusDecoder 1889 - , style "height" "calc(var(--vh, 1vh) * 100)" 1890 - ] 1891 - [ C.overflow_x_hidden 1892 - , C.relative 1893 - , C.scrolling_touch 1894 - , C.w_screen 1895 - , C.z_10 1896 - 1897 - -- 1898 - , ifThenElse scrolling C.overflow_y_auto C.overflow_y_hidden 1899 - ] 1900 - [ brick 1901 - [ style "min-width" "280px" ] 1902 - [ C.flex 1903 - , C.flex_col 1904 - , C.items_center 1905 - , C.h_full 1906 - , C.px_4 1907 - 1908 - -- 1909 - , C.md__px_8 1910 - , C.lg__px_16 1911 - 1912 - -- 1913 - , ifThenElse justifyCenter C.justify_center "" 1914 - ] 1915 - nodes 1916 - ] 1917 - 1918 - 1919 - inputFocusDecoder : Json.Decode.Decoder Msg 1920 - inputFocusDecoder = 1921 - Json.Decode.string 1922 - |> Json.Decode.at [ "target", "tagName" ] 1923 - |> Json.Decode.andThen 1924 - (\targetTagName -> 1925 - case targetTagName of 1926 - "INPUT" -> 1927 - interfaceEventHandler Interface.FocusedOnInput 1928 - 1929 - "TEXTAREA" -> 1930 - interfaceEventHandler Interface.FocusedOnInput 1931 - 1932 - _ -> 1933 - Json.Decode.fail "NOT_INPUT" 1934 - ) 1935 - 1936 - 1937 - interfaceEventHandler : Interface.Msg -> Json.Decode.Decoder UI.Msg 1938 - interfaceEventHandler = 1939 - Interface >> Json.Decode.succeed 1940 - 1941 - 1942 - loadingAnimation : Html msg 1943 - loadingAnimation = 1944 - Html.map never UI.Svg.Elements.loading 1945 - 1946 - 1947 - overlay : Maybe (Alfred Reply) -> Maybe (ContextMenu Reply) -> Html Msg 1948 - overlay maybeAlfred maybeContextMenu = 1949 - let 1950 - isShown = 1951 - Maybe.isJust maybeAlfred || Maybe.isJust maybeContextMenu 1952 - in 1953 - brick 1954 - [ onClick (Interface Interface.HideOverlay) ] 1955 - [ C.inset_0 1956 - , C.bg_black 1957 - , C.fixed 1958 - , C.transition_1000 1959 - , C.transition_ease 1960 - , C.transition_opacity 1961 - , C.z_30 1962 - 1963 - -- 1964 - , ifThenElse isShown "" C.pointer_events_none 1965 - , ifThenElse isShown C.opacity_40 C.opacity_0 1966 - ] 1967 - [] 1968 - 1969 - 1970 - vessel : List (Html Msg) -> Html Msg 1971 - vessel = 1972 - (>>) 1973 - (brick 1974 - [ style "-webkit-mask-image" "-webkit-radial-gradient(white, black)" ] 1975 - [ C.bg_white 1976 - , C.flex 1977 - , C.flex_col 1978 - , C.flex_grow 1979 - , C.overflow_hidden 1980 - , C.relative 1981 - , C.rounded 1982 - 1983 - -- Dark mode 1984 - ------------ 1985 - , C.dark__bg_darkest_hour 1986 - ] 1987 - ) 1988 - (bricky 1989 - [ style "min-height" "296px" ] 1990 - [ C.flex 1991 - , C.flex_grow 1992 - , C.rounded 1993 - , C.shadow_lg 1994 - , C.w_full 1995 - 1996 - -- 1997 - , C.lg__max_w_insulation 1998 - , C.lg__min_w_3xl 1999 - ] 2000 - ) 2001 - 2002 - 2003 - 2004 - -- ⚗️ ░░ HYPAETHRAL DATA 2005 - 2006 - 2007 - gatherSettings : Model -> Settings.Settings 2008 - gatherSettings { audio, backdrop, lastFm, processAutomatically, tracks } = 2009 - { backgroundImage = backdrop.chosen 2010 - , hideDuplicates = tracks.hideDuplicates 2011 - , lastFm = lastFm.sessionKey 2012 - , processAutomatically = processAutomatically 2013 - , rememberProgress = audio.rememberProgress 2014 - } 2015 - 2016 - 2017 - importHypaethral : Json.Decode.Value -> Model -> Return3.Return Model Msg Reply 2018 - importHypaethral value model = 2019 - case decodeHypaethralData value of 2020 - Ok data -> 2021 - let 2022 - { backdrop, sources } = 2023 - model 2024 - 2025 - backdropModel = 2026 - data.settings 2027 - |> Maybe.andThen .backgroundImage 2028 - |> Maybe.withDefault Backdrop.default 2029 - |> Just 2030 - |> (\c -> { backdrop | chosen = c }) 2031 - 2032 - sourcesModel = 2033 - { sources | collection = data.sources } 2034 - 2035 - ( playlistsModel, playlistsCmd, playlistsReplies ) = 2036 - Playlists.importHypaethral model.playlists data 2037 - 2038 - selectedPlaylist = 2039 - Maybe.andThen 2040 - (\n -> List.find (.name >> (==) n) playlistsModel.collection) 2041 - model.playlists.playlistToActivate 2042 - 2043 - ( tracksModel, tracksCmd, tracksReplies ) = 2044 - Tracks.importHypaethral model.tracks data selectedPlaylist 2045 - 2046 - lastFmModel = 2047 - model.lastFm 2048 - in 2049 - ( { model 2050 - | backdrop = backdropModel 2051 - , playlists = playlistsModel 2052 - , sources = sourcesModel 2053 - , tracks = tracksModel 2054 - 2055 - -- 2056 - , lastFm = { lastFmModel | sessionKey = Maybe.andThen .lastFm data.settings } 2057 - , processAutomatically = Maybe.unwrap True .processAutomatically data.settings 2058 - } 2059 - |> Lens.modify 2060 - Audio.lens 2061 - (\a -> 2062 - { a 2063 - | progress = data.progress 2064 - , rememberProgress = Maybe.unwrap True .rememberProgress data.settings 2065 - } 2066 - ) 2067 - -- 2068 - , Cmd.batch 2069 - [ Cmd.map PlaylistsMsg playlistsCmd 2070 - , Cmd.map TracksMsg tracksCmd 2071 - ] 2072 - -- 2073 - , playlistsReplies ++ tracksReplies 2074 - ) 2075 - 2076 - Err err -> 2077 - err 2078 - |> Json.Decode.errorToString 2079 - |> ShowErrorNotification 2080 - |> Return3.returnReplyWithModel model 2081 - 2082 - 2083 - 2084 - -- ⚗️ ░░ ENCLOSED DATA 2085 - 2086 - 2087 - exportEnclosed : Model -> Json.Encode.Value 2088 - exportEnclosed model = 2089 - let 2090 - equalizerSettings = 2091 - { low = model.equalizer.low 2092 - , mid = model.equalizer.mid 2093 - , high = model.equalizer.high 2094 - , volume = model.equalizer.volume 2095 - } 2096 - in 2097 - encodeEnclosedData 2098 - { cachedTracks = model.tracks.cached 2099 - , equalizerSettings = equalizerSettings 2100 - , grouping = model.tracks.grouping 2101 - , onlyShowCachedTracks = model.tracks.cachedOnly 2102 - , onlyShowFavourites = model.tracks.favouritesOnly 2103 - , repeat = model.queue.repeat 2104 - , searchTerm = model.tracks.searchTerm 2105 - , selectedPlaylist = Maybe.map .name model.tracks.selectedPlaylist 2106 - , shuffle = model.queue.shuffle 2107 - , sortBy = model.tracks.sortBy 2108 - , sortDirection = model.tracks.sortDirection 2109 - } 2110 - 2111 - 2112 - importEnclosed : Json.Decode.Value -> Model -> Return3.Return Model Msg Reply 2113 - importEnclosed value model = 2114 - let 2115 - { equalizer, playlists, queue, tracks } = 2116 - model 2117 - in 2118 - case decodeEnclosedData value of 2119 - Ok data -> 2120 - let 2121 - newEqualizer = 2122 - { equalizer 2123 - | low = data.equalizerSettings.low 2124 - , mid = data.equalizerSettings.mid 2125 - , high = data.equalizerSettings.high 2126 - , volume = data.equalizerSettings.volume 2127 - } 2128 - 2129 - newPlaylists = 2130 - { playlists 2131 - | playlistToActivate = data.selectedPlaylist 2132 - } 2133 - 2134 - newQueue = 2135 - { queue 2136 - | repeat = data.repeat 2137 - , shuffle = data.shuffle 2138 - } 2139 - 2140 - newTracks = 2141 - { tracks 2142 - | cached = data.cachedTracks 2143 - , cachedOnly = data.onlyShowCachedTracks 2144 - , favouritesOnly = data.onlyShowFavourites 2145 - , grouping = data.grouping 2146 - , searchTerm = data.searchTerm 2147 - , sortBy = data.sortBy 2148 - , sortDirection = data.sortDirection 2149 - } 2150 - in 2151 - ( { model 2152 - | equalizer = newEqualizer 2153 - , playlists = newPlaylists 2154 - , queue = newQueue 2155 - , tracks = newTracks 2156 - } 2157 - -- 2158 - , Cmd.batch 2159 - [ Cmd.map EqualizerMsg (Equalizer.adjustAllKnobs newEqualizer) 2160 - , Ports.setRepeat data.repeat 2161 - ] 2162 - -- 2163 - , [] 2164 - ) 2165 - 2166 - Err err -> 2167 - Return3.return model
+977
src/Applications/UI/Reply/Translate.elm
··· 1 + module UI.Reply.Translate exposing (..) 2 + 3 + import Alfred exposing (Alfred) 4 + import Alien 5 + import Browser 6 + import Browser.Dom 7 + import Browser.Navigation as Nav 8 + import Chunky exposing (..) 9 + import Common exposing (Switch(..)) 10 + import Conditional exposing (..) 11 + import ContextMenu exposing (ContextMenu) 12 + import Css exposing (url) 13 + import Css.Classes as C 14 + import Debouncer.Basic as Debouncer 15 + import Dict 16 + import Dict.Ext as Dict 17 + import File 18 + import File.Download 19 + import File.Select 20 + import Html exposing (Html, section) 21 + import Html.Attributes exposing (class, id, style) 22 + import Html.Events exposing (on, onClick) 23 + import Html.Events.Extra.Pointer as Pointer 24 + import Html.Lazy as Lazy 25 + import Json.Decode 26 + import Json.Encode 27 + import LastFm 28 + import List.Ext as List 29 + import List.Extra as List 30 + import Maybe.Extra as Maybe 31 + import Monocle.Lens as Lens 32 + import Notifications 33 + import Playlists.Encoding as Playlists 34 + import Process 35 + import Queue 36 + import Return exposing (andThen, return) 37 + import Return.Ext as Return 38 + import Return3 39 + import Settings 40 + import Sources 41 + import Sources.Encoding as Sources 42 + import Sources.Services.Dropbox 43 + import Sources.Services.Google 44 + import String.Ext as String 45 + import Task 46 + import Time 47 + import Tracks 48 + import Tracks.Encoding as Tracks 49 + import UI.Alfred as Alfred 50 + import UI.Audio.State as Audio 51 + import UI.Audio.Types as Audio 52 + import UI.Authentication as Authentication 53 + import UI.Authentication.ContextMenu as Authentication 54 + import UI.Backdrop as Backdrop 55 + import UI.Common.State exposing (showNotification, showNotificationWithModel) 56 + import UI.Console 57 + import UI.ContextMenu 58 + import UI.Demo as Demo 59 + import UI.Equalizer as Equalizer 60 + import UI.Interface.State as Interface 61 + import UI.Interface.Types as Interface 62 + import UI.Navigation as Navigation 63 + import UI.Notifications 64 + import UI.Page as Page 65 + import UI.Playlists as Playlists 66 + import UI.Playlists.Alfred 67 + import UI.Playlists.ContextMenu as Playlists 68 + import UI.Playlists.Directory 69 + import UI.Ports as Ports 70 + import UI.Queue as Queue 71 + import UI.Queue.ContextMenu as Queue 72 + import UI.Reply as Reply exposing (Reply(..)) 73 + import UI.Routing.State as Routing 74 + import UI.Settings as Settings 75 + import UI.Settings.Page 76 + import UI.Sources as Sources 77 + import UI.Sources.ContextMenu as Sources 78 + import UI.Sources.Form 79 + import UI.Sources.Page 80 + import UI.Svg.Elements 81 + import UI.Tracks as Tracks 82 + import UI.Tracks.ContextMenu as Tracks 83 + import UI.Tracks.Scene.List 84 + import UI.Tracks.State as Tracks 85 + import UI.Types as UI exposing (..) 86 + import Url exposing (Protocol(..), Url) 87 + import Url.Ext as Url 88 + import User.Layer exposing (..) 89 + import User.Layer.Methods.RemoteStorage as RemoteStorage 90 + 91 + 92 + 93 + -- 📣 ░░ REPLIES 94 + 95 + 96 + translate : Reply -> UI.Manager 97 + translate reply model = 98 + case reply of 99 + Shunt -> 100 + Return.singleton model 101 + 102 + -- 103 + CopyToClipboard string -> 104 + string 105 + |> Ports.copyToClipboard 106 + |> return model 107 + 108 + GoToPage page -> 109 + Routing.changeUrlUsingPage page model 110 + 111 + StartedDragging -> 112 + Return.singleton { model | isDragging = True } 113 + 114 + ToggleLoadingScreen a -> 115 + Interface.toggleLoadingScreen a model 116 + 117 + ----------------------------------------- 118 + -- Audio 119 + ----------------------------------------- 120 + Seek percentage -> 121 + return model (Ports.seek percentage) 122 + 123 + TogglePlayPause -> 124 + Audio.playPause model 125 + 126 + ToggleRememberProgress -> 127 + model 128 + |> Lens.modify 129 + Audio.lens 130 + (\a -> { a | rememberProgress = not a.rememberProgress }) 131 + |> translate 132 + SaveSettings 133 + 134 + ----------------------------------------- 135 + -- Authentication 136 + ----------------------------------------- 137 + ExternalAuth Blockstack _ -> 138 + Alien.RedirectToBlockstackSignIn 139 + |> Alien.trigger 140 + |> Ports.toBrain 141 + |> return model 142 + 143 + ExternalAuth (Dropbox _) _ -> 144 + [ ( "response_type", "token" ) 145 + , ( "client_id", "te0c9pbeii8f8bw" ) 146 + , ( "redirect_uri", Common.urlOrigin model.url ++ "?action=authenticate/dropbox" ) 147 + ] 148 + |> Common.queryString 149 + |> String.append "https://www.dropbox.com/oauth2/authorize" 150 + |> Nav.load 151 + |> return model 152 + 153 + ExternalAuth (RemoteStorage _) input -> 154 + input 155 + |> RemoteStorage.parseUserAddress 156 + |> Maybe.map 157 + (RemoteStorage.webfingerRequest RemoteStorageWebfinger) 158 + |> Maybe.unwrap 159 + (translate 160 + (ShowErrorNotification RemoteStorage.userAddressError) 161 + model 162 + ) 163 + (return model) 164 + 165 + ExternalAuth _ _ -> 166 + Return.singleton model 167 + 168 + ImportLegacyData -> 169 + Alien.ImportLegacyData 170 + |> Alien.trigger 171 + |> Ports.toBrain 172 + |> return model 173 + |> andThen 174 + (""" 175 + I'll try to import data from Diffuse version one. 176 + If this was successful, you'll get a notification. 177 + """ 178 + |> Notifications.warning 179 + |> showNotification 180 + ) 181 + 182 + PingIpfsForAuth -> 183 + case model.url.protocol of 184 + Https -> 185 + """ 186 + Unfortunately the local IPFS API doesn't work with HTTPS. 187 + Install the [IPFS Companion](https://github.com/ipfs-shipyard/ipfs-companion#release-channel) browser extension to get around this issue 188 + (and make sure it redirects to the local gateway). 189 + """ 190 + |> Notifications.error 191 + |> showNotificationWithModel model 192 + 193 + Http -> 194 + Authentication.PingIpfs 195 + |> AuthenticationMsg 196 + |> Return.performanceF model 197 + 198 + PingTextileForAuth -> 199 + Authentication.PingTextile 200 + |> AuthenticationMsg 201 + |> Return.performanceF model 202 + 203 + ShowUpdateEncryptionKeyScreen authMethod -> 204 + authMethod 205 + |> Authentication.ShowUpdateEncryptionKeyScreen 206 + |> AuthenticationMsg 207 + |> Return.performanceF model 208 + 209 + SignOut -> 210 + let 211 + { playlists, sources, tracks } = 212 + model 213 + in 214 + { model 215 + | authentication = Authentication.Unauthenticated 216 + , playlists = 217 + { playlists 218 + | collection = [] 219 + , playlistToActivate = Maybe.map .name tracks.selectedPlaylist 220 + } 221 + , queue = 222 + Queue.initialModel 223 + , sources = 224 + { sources 225 + | collection = [] 226 + , isProcessing = [] 227 + } 228 + , tracks = 229 + { tracks 230 + | collection = Tracks.emptyCollection 231 + , enabledSourceIds = [] 232 + , favourites = [] 233 + , hideDuplicates = Tracks.initialModel.hideDuplicates 234 + , nowPlaying = Nothing 235 + , searchResults = Nothing 236 + } 237 + } 238 + |> Return.performance (BackdropMsg Backdrop.Default) 239 + |> Return.command (Ports.toBrain <| Alien.trigger Alien.SignOut) 240 + |> Return.command (Ports.toBrain <| Alien.trigger Alien.StopProcessing) 241 + |> Return.command (Ports.activeQueueItemChanged Nothing) 242 + |> Return.command (Nav.pushUrl model.navKey "#/") 243 + 244 + ----------------------------------------- 245 + -- Context Menu 246 + ----------------------------------------- 247 + ContextMenuConfirmation conf r -> 248 + { model | confirmation = Just conf } 249 + |> Return.singleton 250 + |> andThen (translate r) 251 + 252 + ReplyViaContextMenu r -> 253 + case r of 254 + ContextMenuConfirmation _ _ -> 255 + translate r model 256 + 257 + _ -> 258 + translate r { model | contextMenu = Nothing } 259 + 260 + ShowMoreAuthenticationOptions coordinates -> 261 + Return.singleton { model | contextMenu = Just (Authentication.moreOptionsMenu coordinates) } 262 + 263 + ShowPlaylistListMenu coordinates playlist -> 264 + Return.singleton { model | contextMenu = Just (Playlists.listMenu playlist model.tracks.collection.identified model.confirmation coordinates) } 265 + 266 + ShowQueueFutureMenu coordinates { item, itemIndex } -> 267 + Return.singleton { model | contextMenu = Just (Queue.futureMenu { cached = model.tracks.cached, cachingInProgress = model.tracks.cachingInProgress, itemIndex = itemIndex } item coordinates) } 268 + 269 + ShowQueueHistoryMenu coordinates { item } -> 270 + Return.singleton { model | contextMenu = Just (Queue.historyMenu { cached = model.tracks.cached, cachingInProgress = model.tracks.cachingInProgress } item coordinates) } 271 + 272 + ShowSourceContextMenu coordinates source -> 273 + Return.singleton { model | contextMenu = Just (Sources.sourceMenu source coordinates) } 274 + 275 + ShowTracksContextMenu coordinates { alt } tracks -> 276 + let 277 + menuDependencies = 278 + { cached = model.tracks.cached 279 + , cachingInProgress = model.tracks.cachingInProgress 280 + , currentTime = model.currentTime 281 + , selectedPlaylist = model.tracks.selectedPlaylist 282 + , lastModifiedPlaylistName = model.playlists.lastModifiedPlaylist 283 + , showAlternativeMenu = alt 284 + , sources = model.sources.collection 285 + } 286 + in 287 + Return.singleton { model | contextMenu = Just (Tracks.trackMenu menuDependencies tracks coordinates) } 288 + 289 + ShowTracksViewMenu coordinates maybeGrouping -> 290 + Return.singleton { model | contextMenu = Just (Tracks.viewMenu model.tracks.cachedOnly maybeGrouping coordinates) } 291 + 292 + ----------------------------------------- 293 + -- Last.fm 294 + ----------------------------------------- 295 + ConnectLastFm -> 296 + model.url 297 + |> Common.urlOrigin 298 + |> String.addSuffix "?action=authenticate/lastfm" 299 + |> Url.percentEncode 300 + |> String.append "&cb=" 301 + |> String.append 302 + (String.append 303 + "http://www.last.fm/api/auth/?api_key=" 304 + LastFm.apiKey 305 + ) 306 + |> Nav.load 307 + |> return model 308 + 309 + DisconnectLastFm -> 310 + translate 311 + SaveSettings 312 + { model | lastFm = LastFm.disconnect model.lastFm } 313 + 314 + ----------------------------------------- 315 + -- Notifications 316 + ----------------------------------------- 317 + DismissNotification options -> 318 + options 319 + |> UI.Notifications.dismiss model.notifications 320 + |> Return.map (\n -> { model | notifications = n }) 321 + |> Return.mapCmd Reply 322 + 323 + RemoveNotification { id } -> 324 + model.notifications 325 + |> List.filter (Notifications.id >> (/=) id) 326 + |> (\n -> { model | notifications = n }) 327 + |> Return.singleton 328 + 329 + ShowErrorNotification string -> 330 + showNotificationWithModel model (Notifications.error string) 331 + 332 + ShowStickyErrorNotification string -> 333 + showNotificationWithModel model (Notifications.stickyError string) 334 + 335 + ShowStickyErrorNotificationWithCode string code -> 336 + showNotificationWithModel model (Notifications.errorWithCode string code []) 337 + 338 + ShowSuccessNotification string -> 339 + showNotificationWithModel model (Notifications.success string) 340 + 341 + ShowStickySuccessNotification string -> 342 + showNotificationWithModel model (Notifications.stickySuccess string) 343 + 344 + ShowWarningNotification string -> 345 + showNotificationWithModel model (Notifications.warning string) 346 + 347 + ShowStickyWarningNotification string -> 348 + showNotificationWithModel model (Notifications.stickyWarning string) 349 + 350 + ----------------------------------------- 351 + -- Playlists 352 + ----------------------------------------- 353 + ActivatePlaylist playlist -> 354 + playlist 355 + |> Tracks.SelectPlaylist 356 + |> TracksMsg 357 + |> Return.performanceF model 358 + 359 + AddTracksToPlaylist { playlistName, tracks } -> 360 + let 361 + properPlaylistName = 362 + String.trim playlistName 363 + 364 + playlistIndex = 365 + List.findIndex 366 + (\p -> p.autoGenerated == False && p.name == properPlaylistName) 367 + model.playlists.collection 368 + 369 + playlistsModel = 370 + model.playlists 371 + 372 + newCollection = 373 + case playlistIndex of 374 + Just idx -> 375 + List.updateAt 376 + idx 377 + (\p -> { p | tracks = p.tracks ++ tracks }) 378 + playlistsModel.collection 379 + 380 + Nothing -> 381 + (::) 382 + { autoGenerated = False 383 + , name = properPlaylistName 384 + , tracks = tracks 385 + } 386 + playlistsModel.collection 387 + 388 + newModel = 389 + { playlistsModel 390 + | collection = newCollection 391 + , lastModifiedPlaylist = Just properPlaylistName 392 + } 393 + |> (\m -> { model | playlists = m }) 394 + in 395 + (case tracks of 396 + [ t ] -> 397 + "Added __" ++ t.title ++ "__" 398 + 399 + l -> 400 + "Added __" ++ String.fromInt (List.length l) ++ " tracks__" 401 + ) 402 + |> (\s -> s ++ " to the __" ++ properPlaylistName ++ "__ playlist") 403 + |> Notifications.success 404 + |> showNotificationWithModel newModel 405 + |> andThen (translate SavePlaylists) 406 + 407 + DeactivatePlaylist -> 408 + Tracks.DeselectPlaylist 409 + |> TracksMsg 410 + |> Return.performanceF model 411 + 412 + GenerateDirectoryPlaylists -> 413 + let 414 + nonDirectoryPlaylists = 415 + List.filterNot 416 + .autoGenerated 417 + model.playlists.collection 418 + 419 + directoryPlaylists = 420 + UI.Playlists.Directory.generate 421 + model.sources.collection 422 + model.tracks.collection.untouched 423 + 424 + playlists = 425 + model.playlists 426 + in 427 + [ nonDirectoryPlaylists 428 + , directoryPlaylists 429 + ] 430 + |> List.concat 431 + |> (\c -> { playlists | collection = c }) 432 + |> (\p -> { model | playlists = p }) 433 + |> Return.singleton 434 + 435 + RemoveFromSelectedPlaylist playlist tracks -> 436 + let 437 + updatedPlaylist = 438 + Tracks.removeFromPlaylist tracks playlist 439 + 440 + ( tracksModel, playlistsModel ) = 441 + ( model.tracks 442 + , model.playlists 443 + ) 444 + 445 + newPlaylistsCollection = 446 + List.map 447 + (\p -> 448 + if p.name == playlist.name then 449 + updatedPlaylist 450 + 451 + else 452 + p 453 + ) 454 + model.playlists.collection 455 + in 456 + newPlaylistsCollection 457 + |> (\c -> { playlistsModel | collection = c }) 458 + |> (\p -> { model | playlists = p }) 459 + |> Return.performance (TracksMsg <| Tracks.SelectPlaylist updatedPlaylist) 460 + |> andThen (translate SavePlaylists) 461 + 462 + RemovePlaylistFromCollection args -> 463 + args 464 + |> Playlists.RemoveFromCollection 465 + |> PlaylistsMsg 466 + |> Return.performanceF { model | confirmation = Nothing } 467 + 468 + ReplacePlaylistInCollection playlist -> 469 + let 470 + playlists = 471 + model.playlists 472 + in 473 + playlists.collection 474 + |> List.map (\p -> ifThenElse (p.name == playlist.name) playlist p) 475 + |> (\c -> { playlists | collection = c }) 476 + |> (\p -> { model | playlists = p }) 477 + |> translate SavePlaylists 478 + 479 + RequestAssistanceForPlaylists tracks -> 480 + model.playlists.collection 481 + |> List.filterNot .autoGenerated 482 + |> UI.Playlists.Alfred.create tracks 483 + |> Alfred.Assign 484 + |> AlfredMsg 485 + |> Return.performanceF model 486 + 487 + ----------------------------------------- 488 + -- Queue 489 + ----------------------------------------- 490 + ActiveQueueItemChanged maybeQueueItem -> 491 + let 492 + nowPlaying = 493 + Maybe.map .identifiedTrack maybeQueueItem 494 + 495 + portCmd = 496 + maybeQueueItem 497 + |> Maybe.map 498 + (.identifiedTrack >> Tuple.second) 499 + |> Maybe.map 500 + (Queue.makeEngineItem 501 + model.currentTime 502 + model.sources.collection 503 + model.tracks.cached 504 + (if model.audio.rememberProgress then 505 + model.audio.progress 506 + 507 + else 508 + Dict.empty 509 + ) 510 + ) 511 + |> Ports.activeQueueItemChanged 512 + in 513 + model 514 + |> Return.performance (TracksMsg <| Tracks.SetNowPlaying nowPlaying) 515 + |> Return.command portCmd 516 + 517 + AddToQueue { inFront, tracks } -> 518 + (if inFront then 519 + Queue.InjectFirst 520 + 521 + else 522 + Queue.InjectLast 523 + ) 524 + |> (\msg -> msg { showNotification = True } tracks) 525 + |> QueueMsg 526 + |> Return.performanceF model 527 + 528 + FillQueue -> 529 + model.tracks.collection.harvested 530 + |> Queue.Fill model.currentTime 531 + |> QueueMsg 532 + |> Return.performanceF model 533 + 534 + MoveQueueItemToFirst args -> 535 + translate 536 + FillQueue 537 + { model | queue = Queue.moveQueueItemToFirst model.queue args } 538 + 539 + MoveQueueItemToLast args -> 540 + translate 541 + FillQueue 542 + { model | queue = Queue.moveQueueItemToLast model.queue args } 543 + 544 + PlayTrack identifiedTrack -> 545 + identifiedTrack 546 + |> Queue.InjectFirstAndPlay 547 + |> QueueMsg 548 + |> Return.performanceF model 549 + 550 + ResetQueue -> 551 + Return.performance (QueueMsg Queue.Reset) model 552 + 553 + RewindQueue -> 554 + Return.performance (QueueMsg Queue.Rewind) model 555 + 556 + ShiftQueue -> 557 + Return.performance (QueueMsg Queue.Shift) model 558 + 559 + ToggleRepeat -> 560 + Return.performance (QueueMsg Queue.ToggleRepeat) model 561 + 562 + ToggleShuffle -> 563 + Return.performance (QueueMsg Queue.ToggleShuffle) model 564 + 565 + ----------------------------------------- 566 + -- Sources & Tracks 567 + ----------------------------------------- 568 + AddSourceToCollection source -> 569 + source 570 + |> Sources.AddToCollection 571 + |> SourcesMsg 572 + |> Return.performanceF model 573 + 574 + ClearTracksCache -> 575 + model.tracks.cached 576 + |> Json.Encode.list Json.Encode.string 577 + |> Alien.broadcast Alien.RemoveTracksFromCache 578 + |> Ports.toBrain 579 + |> return (Lens.modify Tracks.lens (\m -> { m | cached = [] }) model) 580 + |> andThen (Return.performance <| TracksMsg Tracks.Harvest) 581 + |> andThen (translate <| SaveEnclosedUserData) 582 + |> andThen (translate <| ShowWarningNotification "Tracks cache was cleared") 583 + 584 + DisableTracksGrouping -> 585 + Tracks.DisableGrouping 586 + |> TracksMsg 587 + |> Return.performanceF model 588 + 589 + DownloadTracks zipName tracks -> 590 + let 591 + notification = 592 + Notifications.stickyWarning "Downloading tracks ..." 593 + 594 + downloading = 595 + Just { notificationId = Notifications.id notification } 596 + in 597 + [ ( "zipName", Json.Encode.string zipName ) 598 + , ( "trackIds" 599 + , tracks 600 + |> List.map .id 601 + |> Json.Encode.list Json.Encode.string 602 + ) 603 + ] 604 + |> Json.Encode.object 605 + |> Alien.broadcast Alien.DownloadTracks 606 + |> Ports.toBrain 607 + |> return { model | downloading = downloading } 608 + |> andThen (showNotification notification) 609 + 610 + ExternalSourceAuthorization urlBuilder -> 611 + model.url 612 + |> Common.urlOrigin 613 + |> urlBuilder 614 + |> Nav.load 615 + |> return model 616 + 617 + ForceTracksRerender -> 618 + ( model 619 + , Task.attempt 620 + (always Bypass) 621 + (Browser.Dom.setViewportOf UI.Tracks.Scene.List.containerId 0 1) 622 + ) 623 + 624 + GroupTracksBy grouping -> 625 + grouping 626 + |> Tracks.GroupBy 627 + |> TracksMsg 628 + |> Return.performanceF model 629 + 630 + PreloadNextTrack -> 631 + case List.head model.queue.future of 632 + Just item -> 633 + item 634 + |> .identifiedTrack 635 + |> Tuple.second 636 + |> Queue.makeEngineItem 637 + model.currentTime 638 + model.sources.collection 639 + model.tracks.cached 640 + (if model.audio.rememberProgress then 641 + model.audio.progress 642 + 643 + else 644 + Dict.empty 645 + ) 646 + |> Ports.preloadAudio 647 + |> return model 648 + 649 + Nothing -> 650 + Return.singleton model 651 + 652 + ProcessSources [] -> 653 + Return.singleton model 654 + 655 + ProcessSources sourcesToProcess -> 656 + let 657 + notification = 658 + Notifications.stickyWarning "Processing sources ..." 659 + 660 + notificationId = 661 + Notifications.id notification 662 + 663 + newNotifications = 664 + List.filter 665 + (\n -> Notifications.kind n /= Notifications.Error) 666 + model.notifications 667 + 668 + sources = 669 + model.sources 670 + 671 + isProcessing = 672 + sourcesToProcess 673 + |> List.sortBy (.data >> Dict.fetch "name" "") 674 + |> List.map (\{ id } -> ( id, 0 )) 675 + 676 + newSources = 677 + { sources 678 + | isProcessing = isProcessing 679 + , processingError = Nothing 680 + , processingNotificationId = Just notificationId 681 + } 682 + 683 + newModel = 684 + { model | notifications = newNotifications, sources = newSources } 685 + in 686 + [ ( "origin" 687 + , Json.Encode.string (Common.urlOrigin model.url) 688 + ) 689 + , ( "sources" 690 + , Json.Encode.list Sources.encode sourcesToProcess 691 + ) 692 + ] 693 + |> Json.Encode.object 694 + |> Alien.broadcast Alien.ProcessSources 695 + |> Ports.toBrain 696 + |> return newModel 697 + |> andThen (showNotification notification) 698 + 699 + RemoveSourceFromCollection args -> 700 + args 701 + |> Sources.RemoveFromCollection 702 + |> SourcesMsg 703 + |> Return.performanceF model 704 + 705 + RemoveTracksFromCache tracks -> 706 + let 707 + trackIds = 708 + List.map .id tracks 709 + in 710 + trackIds 711 + |> Json.Encode.list Json.Encode.string 712 + |> Alien.broadcast Alien.RemoveTracksFromCache 713 + |> Ports.toBrain 714 + |> return 715 + (Lens.modify Tracks.lens 716 + (\m -> { m | cached = List.without trackIds m.cached }) 717 + model 718 + ) 719 + |> andThen (Return.performance <| TracksMsg Tracks.Harvest) 720 + |> andThen (translate SaveEnclosedUserData) 721 + 722 + RemoveTracksWithSourceId sourceId -> 723 + let 724 + cmd = 725 + sourceId 726 + |> Json.Encode.string 727 + |> Alien.broadcast Alien.RemoveTracksBySourceId 728 + |> Ports.toBrain 729 + in 730 + sourceId 731 + |> Tracks.RemoveBySourceId 732 + |> TracksMsg 733 + |> Return.performanceF model 734 + |> Return.command cmd 735 + 736 + ReplaceSourceInCollection source -> 737 + let 738 + sources = 739 + model.sources 740 + in 741 + model.sources.collection 742 + |> List.map (\s -> ifThenElse (s.id == source.id) source s) 743 + |> (\c -> { sources | collection = c }) 744 + |> (\s -> { model | sources = s }) 745 + |> Return.singleton 746 + |> andThen (translate SaveSources) 747 + 748 + ScrollToNowPlaying -> 749 + Return.performance (TracksMsg Tracks.ScrollToNowPlaying) model 750 + 751 + StoreTracksInCache tracks -> 752 + let 753 + trackIds = 754 + List.map .id tracks 755 + 756 + notification = 757 + case tracks of 758 + [ t ] -> 759 + ("__" ++ t.tags.title ++ "__ will be stored in the cache") 760 + |> Notifications.success 761 + 762 + list -> 763 + list 764 + |> List.length 765 + |> String.fromInt 766 + |> (\s -> "__" ++ s ++ " tracks__ will be stored in the cache") 767 + |> Notifications.success 768 + in 769 + tracks 770 + |> Json.Encode.list 771 + (\track -> 772 + Json.Encode.object 773 + [ ( "trackId" 774 + , Json.Encode.string track.id 775 + ) 776 + , ( "url" 777 + , track 778 + |> Queue.makeTrackUrl 779 + model.currentTime 780 + model.sources.collection 781 + |> Json.Encode.string 782 + ) 783 + ] 784 + ) 785 + |> Alien.broadcast Alien.StoreTracksInCache 786 + |> Ports.toBrain 787 + |> return 788 + (Lens.modify Tracks.lens 789 + (\m -> { m | cachingInProgress = m.cachingInProgress ++ trackIds }) 790 + model 791 + ) 792 + |> andThen (showNotification notification) 793 + 794 + ToggleCachedTracksOnly -> 795 + Return.performance (TracksMsg Tracks.ToggleCachedOnly) model 796 + 797 + ToggleDirectoryPlaylists args -> 798 + Return.performance (SourcesMsg <| Sources.ToggleDirectoryPlaylists args) model 799 + 800 + ToggleHideDuplicates -> 801 + Return.performance (TracksMsg Tracks.ToggleHideDuplicates) model 802 + 803 + ToggleProcessAutomatically -> 804 + translate SaveSettings { model | processAutomatically = not model.processAutomatically } 805 + 806 + ----------------------------------------- 807 + -- User Data 808 + ----------------------------------------- 809 + ChooseBackdrop filename -> 810 + filename 811 + |> Backdrop.Choose 812 + |> BackdropMsg 813 + |> Return.performanceF model 814 + 815 + Export -> 816 + { favourites = model.tracks.favourites 817 + , playlists = List.filterNot .autoGenerated model.playlists.collection 818 + , progress = model.audio.progress 819 + , settings = Just (gatherSettings model) 820 + , sources = model.sources.collection 821 + , tracks = model.tracks.collection.untouched 822 + } 823 + |> encodeHypaethralData 824 + |> Json.Encode.encode 2 825 + |> File.Download.string "diffuse.json" "application/json" 826 + |> return model 827 + 828 + InsertDemo -> 829 + model.currentTime 830 + |> Demo.tape 831 + |> LoadHypaethralUserData 832 + |> Return.performanceF model 833 + |> saveAllHypaethralData 834 + 835 + LoadDefaultBackdrop -> 836 + Backdrop.Default 837 + |> BackdropMsg 838 + |> Return.performanceF model 839 + 840 + RequestImport -> 841 + ImportFile 842 + |> File.Select.file [ "application/json" ] 843 + |> return model 844 + 845 + SaveEnclosedUserData -> 846 + model 847 + |> exportEnclosed 848 + |> Alien.broadcast Alien.SaveEnclosedUserData 849 + |> Ports.toBrain 850 + |> return model 851 + 852 + SaveFavourites -> 853 + model.tracks.favourites 854 + |> Json.Encode.list Tracks.encodeFavourite 855 + |> Alien.broadcast Alien.SaveFavourites 856 + |> Ports.toBrain 857 + |> return model 858 + 859 + SavePlaylists -> 860 + model.playlists.collection 861 + |> List.filterNot .autoGenerated 862 + |> Json.Encode.list Playlists.encode 863 + |> Alien.broadcast Alien.SavePlaylists 864 + |> Ports.toBrain 865 + |> return model 866 + 867 + SaveProgress -> 868 + model.audio.progress 869 + |> Json.Encode.dict identity Json.Encode.float 870 + |> Alien.broadcast Alien.SaveProgress 871 + |> Ports.toBrain 872 + |> return model 873 + 874 + SaveSettings -> 875 + model 876 + |> gatherSettings 877 + |> Settings.encode 878 + |> Alien.broadcast Alien.SaveSettings 879 + |> Ports.toBrain 880 + |> return model 881 + 882 + SaveSources -> 883 + let 884 + updateEnabledSourceIdsOnTracks = 885 + model.sources.collection 886 + |> Sources.enabledSourceIds 887 + |> Tracks.SetEnabledSourceIds 888 + |> TracksMsg 889 + |> Return.performance 890 + 891 + ( updatedModel, updatedCmd ) = 892 + updateEnabledSourceIdsOnTracks model 893 + in 894 + updatedModel.sources.collection 895 + |> Json.Encode.list Sources.encode 896 + |> Alien.broadcast Alien.SaveSources 897 + |> Ports.toBrain 898 + |> Return.return updatedModel 899 + |> Return.command updatedCmd 900 + 901 + SaveTracks -> 902 + model.tracks.collection.untouched 903 + |> Json.Encode.list Tracks.encodeTrack 904 + |> Alien.broadcast Alien.SaveTracks 905 + |> Ports.toBrain 906 + |> return model 907 + 908 + 909 + translateWithModel : Model -> Reply -> ( Model, Cmd Msg ) 910 + translateWithModel model reply = 911 + translate reply model 912 + 913 + 914 + saveAllHypaethralData : ( Model, Cmd Msg ) -> ( Model, Cmd Msg ) 915 + saveAllHypaethralData return = 916 + List.foldl 917 + (\( _, bit ) -> 918 + case bit of 919 + Favourites -> 920 + andThen (translate SaveFavourites) 921 + 922 + Playlists -> 923 + andThen (translate SavePlaylists) 924 + 925 + Progress -> 926 + andThen (translate SaveProgress) 927 + 928 + Settings -> 929 + andThen (translate SaveSettings) 930 + 931 + Sources -> 932 + andThen (translate SaveSources) 933 + 934 + Tracks -> 935 + andThen (translate SaveTracks) 936 + ) 937 + return 938 + hypaethralBit.list 939 + 940 + 941 + 942 + -- USER 943 + 944 + 945 + exportEnclosed : Model -> Json.Encode.Value 946 + exportEnclosed model = 947 + let 948 + equalizerSettings = 949 + { low = model.equalizer.low 950 + , mid = model.equalizer.mid 951 + , high = model.equalizer.high 952 + , volume = model.equalizer.volume 953 + } 954 + in 955 + encodeEnclosedData 956 + { cachedTracks = model.tracks.cached 957 + , equalizerSettings = equalizerSettings 958 + , grouping = model.tracks.grouping 959 + , onlyShowCachedTracks = model.tracks.cachedOnly 960 + , onlyShowFavourites = model.tracks.favouritesOnly 961 + , repeat = model.queue.repeat 962 + , searchTerm = model.tracks.searchTerm 963 + , selectedPlaylist = Maybe.map .name model.tracks.selectedPlaylist 964 + , shuffle = model.queue.shuffle 965 + , sortBy = model.tracks.sortBy 966 + , sortDirection = model.tracks.sortDirection 967 + } 968 + 969 + 970 + gatherSettings : Model -> Settings.Settings 971 + gatherSettings { audio, backdrop, lastFm, processAutomatically, tracks } = 972 + { backgroundImage = backdrop.chosen 973 + , hideDuplicates = tracks.hideDuplicates 974 + , lastFm = lastFm.sessionKey 975 + , processAutomatically = processAutomatically 976 + , rememberProgress = audio.rememberProgress 977 + }
+164
src/Applications/UI/Routing/State.elm
··· 1 + module UI.Routing.State exposing (changeUrlUsingPage, linkClicked, resetUrl, transition, urlChanged) 2 + 3 + import Browser exposing (UrlRequest) 4 + import Browser.Navigation as Nav 5 + import List.Extra as List 6 + import Management 7 + import Monocle.Lens as Lens 8 + import Return exposing (return) 9 + import Return.Ext as Return exposing (communicate) 10 + import Sources 11 + import Sources.Services.Dropbox 12 + import Sources.Services.Google 13 + import UI.Page as Page exposing (Page) 14 + import UI.Sources.Form 15 + import UI.Sources.Page 16 + import UI.Sources.State as Sources 17 + import UI.Tracks.State as Tracks 18 + import UI.Types as UI exposing (Manager) 19 + import Url exposing (Url) 20 + 21 + 22 + 23 + -- 📣 24 + 25 + 26 + changeUrlUsingPage : Page -> UI.Manager 27 + changeUrlUsingPage page model = 28 + page 29 + |> Page.toString 30 + |> Nav.pushUrl model.navKey 31 + |> return model 32 + 33 + 34 + linkClicked : UrlRequest -> UI.Manager 35 + linkClicked urlRequest model = 36 + case urlRequest of 37 + Browser.Internal urlWithFragment -> 38 + let 39 + url = 40 + if urlWithFragment.fragment == Just "/" then 41 + { urlWithFragment | fragment = Nothing } 42 + 43 + else 44 + urlWithFragment 45 + in 46 + if url.path /= model.url.path then 47 + return model (Nav.load url.path) 48 + 49 + else 50 + return model (Nav.pushUrl model.navKey <| Url.toString url) 51 + 52 + Browser.External href -> 53 + return model (Nav.load href) 54 + 55 + 56 + urlChanged : Url -> UI.Manager 57 + urlChanged url model = 58 + let 59 + rewrittenUrl = 60 + Page.rewriteUrl { url | query = Nothing } 61 + in 62 + case ( url.query, Page.fromUrl rewrittenUrl ) of 63 + ( Nothing, Just page ) -> 64 + transition page { model | page = page, url = url } 65 + 66 + ( Just _, Just page ) -> 67 + return model (resetUrl model.navKey url page) 68 + 69 + _ -> 70 + return model (resetUrl model.navKey url Page.Index) 71 + 72 + 73 + 74 + -- TRANSITIONING 75 + 76 + 77 + transition : Page -> UI.Manager 78 + transition page model = 79 + case page of 80 + ----------------------------------------- 81 + -- Sources.NewThroughRedirect 82 + ----------------------------------------- 83 + Page.Sources (UI.Sources.Page.NewThroughRedirect service args) -> 84 + let 85 + ( sources, form, defaultContext ) = 86 + ( model.sources 87 + , model.sources.form 88 + , UI.Sources.Form.defaultContext 89 + ) 90 + in 91 + { defaultContext 92 + | data = 93 + case service of 94 + Sources.Dropbox -> 95 + Sources.Services.Dropbox.authorizationSourceData args 96 + 97 + Sources.Google -> 98 + Sources.Services.Google.authorizationSourceData args 99 + 100 + _ -> 101 + defaultContext.data 102 + , service = 103 + service 104 + } 105 + |> (\c -> { form | context = c, step = UI.Sources.Form.By }) 106 + |> (\f -> { sources | form = f }) 107 + |> (\s -> { model | sources = s }) 108 + |> Return.singleton 109 + 110 + ----------------------------------------- 111 + -- Sources.Edit 112 + ----------------------------------------- 113 + Page.Sources (UI.Sources.Page.Edit sourceId) -> 114 + loadSourceForForm sourceId model 115 + 116 + ----------------------------------------- 117 + -- Sources.Rename 118 + ----------------------------------------- 119 + Page.Sources (UI.Sources.Page.Rename sourceId) -> 120 + loadSourceForForm sourceId model 121 + 122 + ----------------------------------------- 123 + -- 📭 124 + ----------------------------------------- 125 + _ -> 126 + Return.singleton model 127 + 128 + 129 + 130 + -- 🚀 131 + 132 + 133 + resetUrl : Nav.Key -> Url -> Page.Page -> Cmd UI.Msg 134 + resetUrl key url page = 135 + Nav.replaceUrl key (url.path ++ Page.toString page) 136 + 137 + 138 + 139 + -- ㊙️ 140 + 141 + 142 + loadSourceForForm : String -> UI.Manager 143 + loadSourceForForm sourceId model = 144 + let 145 + isLoading = 146 + model.isLoading 147 + 148 + maybeSource = 149 + List.find (.id >> (==) sourceId) model.sources.collection 150 + in 151 + case ( isLoading, maybeSource ) of 152 + ( False, Just source ) -> 153 + model 154 + |> Lens.modify Sources.formLens (\m -> { m | context = source }) 155 + |> Return.singleton 156 + 157 + ( False, Nothing ) -> 158 + Return.singleton model 159 + 160 + ( True, _ ) -> 161 + -- Redirect away from edit-source page 162 + changeUrlUsingPage 163 + (Page.Sources UI.Sources.Page.Index) 164 + model
+9 -1
src/Applications/UI/Sources/State.elm
··· 9 9 10 10 lens = 11 11 { get = .sources 12 - , set = \sources ui -> { ui | sources = sources } 12 + , set = \sources m -> { m | sources = sources } 13 13 } 14 + 15 + 16 + formLens = 17 + Lens.compose 18 + lens 19 + { get = .form 20 + , set = \form m -> { m | form = form } 21 + }
+4
src/Applications/UI/Tracks/State.elm
··· 11 11 { get = .tracks 12 12 , set = \tracks ui -> { ui | tracks = tracks } 13 13 } 14 + 15 + 16 + 17 + -- 🔱
+15 -21
src/Applications/UI/Types.elm
··· 153 153 = Bypass 154 154 | Reply Reply 155 155 -- 156 - | DownloadTracksFinished 157 - | LoadEnclosedUserData Json.Decode.Value 158 - | LoadHypaethralUserData Json.Decode.Value 156 + | Audio Audio.Msg 157 + | Interface Interface.Msg 159 158 ----------------------------------------- 160 159 -- Authentication 161 160 ----------------------------------------- ··· 175 174 | SourcesMsg Sources.Msg 176 175 | TracksMsg Tracks.Msg 177 176 ----------------------------------------- 178 - -- Import / Export 177 + -- Routing 179 178 ----------------------------------------- 180 - | Import File 181 - | ImportJson String 179 + | ChangeUrlUsingPage Page 180 + | LinkClicked Browser.UrlRequest 181 + | PageChanged Page 182 + | UrlChanged Url 182 183 ----------------------------------------- 183 - -- Last.fm 184 + -- Services 184 185 ----------------------------------------- 185 186 | GotLastFmSession (Result Http.Error String) 186 187 | Scrobble { duration : Int, timestamp : Int, trackId : String } 187 188 ----------------------------------------- 188 - -- Page Transitions 189 - ----------------------------------------- 190 - | PageChanged Page 189 + -- Tracks 191 190 ----------------------------------------- 192 - -- Tracks Cache 193 - ----------------------------------------- 191 + | DownloadTracksFinished 194 192 | FailedToStoreTracksInCache (List String) 195 193 | FinishedStoringTracksInCache (List String) 196 194 ----------------------------------------- 197 - -- URL 198 - ----------------------------------------- 199 - | ChangeUrlUsingPage Page 200 - | LinkClicked Browser.UrlRequest 201 - | UrlChanged Url 195 + -- User 202 196 ----------------------------------------- 203 - -- TARGET => Pieces 204 - ----------------------------------------- 205 - | Audio Audio.Msg 206 - | Interface Interface.Msg 197 + | ImportFile File 198 + | ImportJson String 199 + | LoadEnclosedUserData Json.Decode.Value 200 + | LoadHypaethralUserData Json.Decode.Value 207 201 208 202 209 203 type alias Organizer model =
+252
src/Applications/UI/User/State.elm
··· 1 + module UI.User.State exposing (..) 2 + 3 + import File exposing (File) 4 + import Json.Decode 5 + import Json.Encode 6 + import LastFm 7 + import List.Extra as List 8 + import Management 9 + import Maybe.Extra as Maybe 10 + import Monocle.Lens as Lens 11 + import Notifications 12 + import Process 13 + import Return exposing (andThen, return) 14 + import Return.Ext as Return exposing (communicate) 15 + import Return3 16 + import Task 17 + import UI.Audio.State as Audio 18 + import UI.Backdrop as Backdrop 19 + import UI.Common.State as Common exposing (showNotification) 20 + import UI.Equalizer as Equalizer 21 + import UI.Page as Page exposing (Page) 22 + import UI.Playlists as Playlists 23 + import UI.Ports as Ports 24 + import UI.Reply exposing (..) 25 + import UI.Reply.Translate as Reply 26 + import UI.Routing.State as Routing 27 + import UI.Sources as Sources 28 + import UI.Tracks as Tracks 29 + import UI.Types as UI exposing (..) 30 + import Url.Ext as Url 31 + import User.Layer exposing (..) 32 + 33 + 34 + 35 + -- 🔱 36 + 37 + 38 + importFile : File -> UI.Manager 39 + importFile file model = 40 + 250 41 + |> Process.sleep 42 + |> Task.andThen (\_ -> File.toString file) 43 + |> Task.perform UI.ImportJson 44 + |> return { model | isLoading = True } 45 + 46 + 47 + importJson : String -> UI.Manager 48 + importJson json model = 49 + json 50 + -- Load data on main thread (this app) 51 + |> Json.Decode.decodeString Json.Decode.value 52 + |> Result.withDefault Json.Encode.null 53 + |> (\j -> importHypaethral j model) 54 + |> Return3.wield Reply.translate 55 + -- Show notification 56 + |> andThen 57 + ("Imported data successfully!" 58 + |> Notifications.success 59 + |> showNotification 60 + ) 61 + -- Clear tracks cache 62 + |> andThen (Reply.translate ClearTracksCache) 63 + -- Redirect to index page 64 + |> andThen (Routing.changeUrlUsingPage Page.Index) 65 + ----------------------------- 66 + -- Save all the imported data 67 + ----------------------------- 68 + |> Reply.saveAllHypaethralData 69 + 70 + 71 + loadEnclosedUserData : Json.Decode.Value -> UI.Manager 72 + loadEnclosedUserData json model = 73 + model 74 + |> importEnclosed json 75 + |> Return3.wield Reply.translate 76 + 77 + 78 + loadHypaethralUserData : Json.Decode.Value -> UI.Manager 79 + loadHypaethralUserData json model = 80 + model 81 + |> importHypaethral json 82 + |> Return3.wield Reply.translate 83 + |> andThen 84 + (\m -> 85 + case Url.action m.url of 86 + [ "authenticate", "lastfm" ] -> 87 + { authenticating = True 88 + , sessionKey = Nothing 89 + } 90 + |> (\n -> { m | lastFm = n }) 91 + |> communicate (LastFm.authenticationCommand GotLastFmSession m.url) 92 + 93 + _ -> 94 + Return.singleton m 95 + ) 96 + |> andThen 97 + (\m -> 98 + if m.isUpgrading then 99 + """ 100 + Thank you for using Diffuse V1! 101 + If you want to import your old data, 102 + please go to the [import page](#/settings/import-export). 103 + """ 104 + |> Notifications.stickySuccess 105 + |> Common.showNotificationWithModel m 106 + 107 + else 108 + Return.singleton m 109 + ) 110 + |> andThen 111 + (\m -> 112 + if m.processAutomatically then 113 + m.sources 114 + |> Sources.sourcesToProcess 115 + |> ProcessSources 116 + |> Reply.translateWithModel m 117 + 118 + else 119 + Return.singleton m 120 + ) 121 + 122 + 123 + 124 + -- ⚗️ ░░ HYPAETHRAL DATA 125 + 126 + 127 + importHypaethral : Json.Decode.Value -> Model -> Return3.Return Model Msg Reply 128 + importHypaethral value model = 129 + case decodeHypaethralData value of 130 + Ok data -> 131 + let 132 + { backdrop, sources } = 133 + model 134 + 135 + backdropModel = 136 + data.settings 137 + |> Maybe.andThen .backgroundImage 138 + |> Maybe.withDefault Backdrop.default 139 + |> Just 140 + |> (\c -> { backdrop | chosen = c }) 141 + 142 + sourcesModel = 143 + { sources | collection = data.sources } 144 + 145 + ( playlistsModel, playlistsCmd, playlistsReplies ) = 146 + Playlists.importHypaethral model.playlists data 147 + 148 + selectedPlaylist = 149 + Maybe.andThen 150 + (\n -> List.find (.name >> (==) n) playlistsModel.collection) 151 + model.playlists.playlistToActivate 152 + 153 + ( tracksModel, tracksCmd, tracksReplies ) = 154 + Tracks.importHypaethral model.tracks data selectedPlaylist 155 + 156 + lastFmModel = 157 + model.lastFm 158 + in 159 + ( { model 160 + | backdrop = backdropModel 161 + , playlists = playlistsModel 162 + , sources = sourcesModel 163 + , tracks = tracksModel 164 + 165 + -- 166 + , lastFm = { lastFmModel | sessionKey = Maybe.andThen .lastFm data.settings } 167 + , processAutomatically = Maybe.unwrap True .processAutomatically data.settings 168 + } 169 + |> Lens.modify 170 + Audio.lens 171 + (\a -> 172 + { a 173 + | progress = data.progress 174 + , rememberProgress = Maybe.unwrap True .rememberProgress data.settings 175 + } 176 + ) 177 + -- 178 + , Cmd.batch 179 + [ Cmd.map PlaylistsMsg playlistsCmd 180 + , Cmd.map TracksMsg tracksCmd 181 + ] 182 + -- 183 + , playlistsReplies ++ tracksReplies 184 + ) 185 + 186 + Err err -> 187 + err 188 + |> Json.Decode.errorToString 189 + |> ShowErrorNotification 190 + |> Return3.returnReplyWithModel model 191 + 192 + 193 + 194 + -- ⚗️ ░░ ENCLOSED DATA 195 + 196 + 197 + importEnclosed : Json.Decode.Value -> Model -> Return3.Return Model Msg Reply 198 + importEnclosed value model = 199 + let 200 + { equalizer, playlists, queue, tracks } = 201 + model 202 + in 203 + case decodeEnclosedData value of 204 + Ok data -> 205 + let 206 + newEqualizer = 207 + { equalizer 208 + | low = data.equalizerSettings.low 209 + , mid = data.equalizerSettings.mid 210 + , high = data.equalizerSettings.high 211 + , volume = data.equalizerSettings.volume 212 + } 213 + 214 + newPlaylists = 215 + { playlists 216 + | playlistToActivate = data.selectedPlaylist 217 + } 218 + 219 + newQueue = 220 + { queue 221 + | repeat = data.repeat 222 + , shuffle = data.shuffle 223 + } 224 + 225 + newTracks = 226 + { tracks 227 + | cached = data.cachedTracks 228 + , cachedOnly = data.onlyShowCachedTracks 229 + , favouritesOnly = data.onlyShowFavourites 230 + , grouping = data.grouping 231 + , searchTerm = data.searchTerm 232 + , sortBy = data.sortBy 233 + , sortDirection = data.sortDirection 234 + } 235 + in 236 + ( { model 237 + | equalizer = newEqualizer 238 + , playlists = newPlaylists 239 + , queue = newQueue 240 + , tracks = newTracks 241 + } 242 + -- 243 + , Cmd.batch 244 + [ Cmd.map EqualizerMsg (Equalizer.adjustAllKnobs newEqualizer) 245 + , Ports.setRepeat data.repeat 246 + ] 247 + -- 248 + , [] 249 + ) 250 + 251 + Err err -> 252 + Return3.return model
+402
src/Applications/UI/View.elm
··· 1 + module UI.View exposing (view) 2 + 3 + import Alfred exposing (Alfred) 4 + import Alien 5 + import Browser 6 + import Browser.Dom 7 + import Browser.Navigation as Nav 8 + import Chunky exposing (..) 9 + import Common exposing (Switch(..)) 10 + import Conditional exposing (..) 11 + import ContextMenu exposing (ContextMenu) 12 + import Css exposing (url) 13 + import Css.Classes as C 14 + import Debouncer.Basic as Debouncer 15 + import Dict 16 + import Dict.Ext as Dict 17 + import File 18 + import File.Download 19 + import File.Select 20 + import Html exposing (Html, section) 21 + import Html.Attributes exposing (class, id, style) 22 + import Html.Events exposing (on, onClick) 23 + import Html.Events.Extra.Pointer as Pointer 24 + import Html.Lazy as Lazy 25 + import Json.Decode 26 + import Json.Encode 27 + import LastFm 28 + import List.Ext as List 29 + import List.Extra as List 30 + import Maybe.Extra as Maybe 31 + import Monocle.Lens as Lens 32 + import Notifications 33 + import Playlists.Encoding as Playlists 34 + import Process 35 + import Queue 36 + import Return2 exposing (..) 37 + import Return3 38 + import Settings 39 + import Sources 40 + import Sources.Encoding as Sources 41 + import Sources.Services.Dropbox 42 + import Sources.Services.Google 43 + import String.Ext as String 44 + import Task 45 + import Time 46 + import Tracks 47 + import Tracks.Encoding as Tracks 48 + import UI.Alfred as Alfred 49 + import UI.Audio.State as Audio 50 + import UI.Audio.Types as Audio 51 + import UI.Authentication as Authentication 52 + import UI.Authentication.ContextMenu as Authentication 53 + import UI.Backdrop as Backdrop 54 + import UI.Common.State exposing (showNotification, showNotificationWithModel) 55 + import UI.Console 56 + import UI.ContextMenu 57 + import UI.Demo as Demo 58 + import UI.Equalizer as Equalizer 59 + import UI.Interface.State as Interface 60 + import UI.Interface.Types as Interface 61 + import UI.Navigation as Navigation 62 + import UI.Notifications 63 + import UI.Page as Page 64 + import UI.Playlists as Playlists 65 + import UI.Playlists.Alfred 66 + import UI.Playlists.ContextMenu as Playlists 67 + import UI.Playlists.Directory 68 + import UI.Ports as Ports 69 + import UI.Queue as Queue 70 + import UI.Queue.ContextMenu as Queue 71 + import UI.Reply as Reply exposing (Reply(..)) 72 + import UI.Routing.State as Routing 73 + import UI.Settings as Settings 74 + import UI.Settings.Page 75 + import UI.Sources as Sources 76 + import UI.Sources.ContextMenu as Sources 77 + import UI.Sources.Form 78 + import UI.Sources.Page 79 + import UI.Svg.Elements 80 + import UI.Tracks as Tracks 81 + import UI.Tracks.ContextMenu as Tracks 82 + import UI.Tracks.Scene.List 83 + import UI.Tracks.State as Tracks 84 + import UI.Types as UI exposing (..) 85 + import UI.User.State as User 86 + import Url exposing (Protocol(..), Url) 87 + import Url.Ext as Url 88 + import User.Layer exposing (..) 89 + import User.Layer.Methods.RemoteStorage as RemoteStorage 90 + 91 + 92 + 93 + -- 🗺 94 + 95 + 96 + view : Model -> Browser.Document Msg 97 + view model = 98 + { title = "Diffuse" 99 + , body = [ body model ] 100 + } 101 + 102 + 103 + body : Model -> Html Msg 104 + body model = 105 + section 106 + (if Maybe.isJust model.contextMenu || Maybe.isJust model.alfred.instance then 107 + [ on "tap" (interfaceEventHandler Interface.HideOverlay) ] 108 + 109 + else if Maybe.isJust model.equalizer.activeKnob then 110 + [ Pointer.onMove (EqualizerMsg << Equalizer.AdjustKnob) 111 + , Pointer.onUp (EqualizerMsg << Equalizer.DeactivateKnob) 112 + , Pointer.onCancel (EqualizerMsg << Equalizer.DeactivateKnob) 113 + ] 114 + 115 + else if model.isDragging then 116 + [ class C.dragging_something 117 + , on "mouseup" (interfaceEventHandler Interface.StoppedDragging) 118 + , on "touchcancel" (interfaceEventHandler Interface.StoppedDragging) 119 + , on "touchend" (interfaceEventHandler Interface.StoppedDragging) 120 + ] 121 + 122 + else if Maybe.isJust model.queue.selection then 123 + [ on "tap" (interfaceEventHandler Interface.RemoveQueueSelection) ] 124 + 125 + else if not (List.isEmpty model.tracks.selectedTrackIndexes) then 126 + [ on "tap" (interfaceEventHandler Interface.RemoveTrackSelection) ] 127 + 128 + else 129 + [] 130 + ) 131 + [ ----------------------------------------- 132 + -- Alfred 133 + ----------------------------------------- 134 + model.alfred 135 + |> Lazy.lazy Alfred.view 136 + |> Html.map AlfredMsg 137 + 138 + ----------------------------------------- 139 + -- Backdrop 140 + ----------------------------------------- 141 + , model.backdrop 142 + |> Lazy.lazy Backdrop.view 143 + |> Html.map BackdropMsg 144 + 145 + ----------------------------------------- 146 + -- Context Menu 147 + ----------------------------------------- 148 + , model.contextMenu 149 + |> Lazy.lazy UI.ContextMenu.view 150 + |> Html.map Reply 151 + 152 + ----------------------------------------- 153 + -- Notifications 154 + ----------------------------------------- 155 + , model.notifications 156 + |> Lazy.lazy UI.Notifications.view 157 + |> Html.map Reply 158 + 159 + ----------------------------------------- 160 + -- Overlay 161 + ----------------------------------------- 162 + , model.contextMenu 163 + |> Lazy.lazy2 overlay model.alfred.instance 164 + 165 + ----------------------------------------- 166 + -- Content 167 + ----------------------------------------- 168 + , let 169 + opts = 170 + { justifyCenter = False 171 + , scrolling = not model.isDragging 172 + } 173 + in 174 + case ( model.isLoading, model.authentication ) of 175 + ( True, _ ) -> 176 + content { opts | justifyCenter = True } [ loadingAnimation ] 177 + 178 + ( False, Authentication.Authenticated _ ) -> 179 + content opts (defaultScreen model) 180 + 181 + ( False, _ ) -> 182 + model.authentication 183 + |> Lazy.lazy Authentication.view 184 + |> Html.map AuthenticationMsg 185 + |> List.singleton 186 + |> content opts 187 + ] 188 + 189 + 190 + defaultScreen : Model -> List (Html Msg) 191 + defaultScreen model = 192 + [ Lazy.lazy2 193 + (Navigation.global 194 + [ ( Page.Index, "Tracks" ) 195 + , ( Page.Sources UI.Sources.Page.Index, "Sources" ) 196 + , ( Page.Settings UI.Settings.Page.Index, "Settings" ) 197 + ] 198 + ) 199 + model.alfred.instance 200 + model.page 201 + 202 + ----------------------------------------- 203 + -- Main 204 + ----------------------------------------- 205 + , vessel 206 + [ { amountOfSources = List.length model.sources.collection 207 + , bgColor = model.backdrop.bgColor 208 + , darkMode = model.darkMode 209 + , isOnIndexPage = model.page == Page.Index 210 + , isTouchDevice = model.isTouchDevice 211 + , sourceIdsBeingProcessed = List.map Tuple.first model.sources.isProcessing 212 + , viewport = model.viewport 213 + } 214 + |> Tracks.view model.tracks 215 + |> Html.map TracksMsg 216 + 217 + -- Pages 218 + -------- 219 + , case model.page of 220 + Page.Equalizer -> 221 + model.equalizer 222 + |> Lazy.lazy Equalizer.view 223 + |> Html.map EqualizerMsg 224 + 225 + Page.Index -> 226 + nothing 227 + 228 + Page.Playlists subPage -> 229 + model.backdrop.bgColor 230 + |> Lazy.lazy4 231 + Playlists.view 232 + subPage 233 + model.playlists 234 + model.tracks.selectedPlaylist 235 + |> Html.map PlaylistsMsg 236 + 237 + Page.Queue subPage -> 238 + model.queue 239 + |> Lazy.lazy2 Queue.view subPage 240 + |> Html.map QueueMsg 241 + 242 + Page.Settings subPage -> 243 + { authenticationMethod = Authentication.extractMethod model.authentication 244 + , chosenBackgroundImage = model.backdrop.chosen 245 + , hideDuplicateTracks = model.tracks.hideDuplicates 246 + , lastFm = model.lastFm 247 + , processAutomatically = model.processAutomatically 248 + , rememberProgress = model.audio.rememberProgress 249 + } 250 + |> Lazy.lazy2 Settings.view subPage 251 + |> Html.map Reply 252 + 253 + Page.Sources subPage -> 254 + let 255 + amountOfTracks = 256 + List.length model.tracks.collection.untouched 257 + in 258 + model.sources 259 + |> Lazy.lazy3 Sources.view { amountOfTracks = amountOfTracks } subPage 260 + |> Html.map SourcesMsg 261 + ] 262 + 263 + ----------------------------------------- 264 + -- Controls 265 + ----------------------------------------- 266 + , Html.map Reply 267 + (UI.Console.view 268 + model.queue.activeItem 269 + model.queue.repeat 270 + model.queue.shuffle 271 + { stalled = model.audio.hasStalled 272 + , loading = model.audio.isLoading 273 + , playing = model.audio.isPlaying 274 + } 275 + ( model.audio.position 276 + , model.audio.duration 277 + ) 278 + ) 279 + ] 280 + 281 + 282 + 283 + -- 🗺 ░░ BITS 284 + 285 + 286 + content : { justifyCenter : Bool, scrolling : Bool } -> List (Html Msg) -> Html Msg 287 + content { justifyCenter, scrolling } nodes = 288 + brick 289 + [ on "focusout" (interfaceEventHandler Interface.Blur) 290 + , on "focusin" inputFocusDecoder 291 + , style "height" "calc(var(--vh, 1vh) * 100)" 292 + ] 293 + [ C.overflow_x_hidden 294 + , C.relative 295 + , C.scrolling_touch 296 + , C.w_screen 297 + , C.z_10 298 + 299 + -- 300 + , ifThenElse scrolling C.overflow_y_auto C.overflow_y_hidden 301 + ] 302 + [ brick 303 + [ style "min-width" "280px" ] 304 + [ C.flex 305 + , C.flex_col 306 + , C.items_center 307 + , C.h_full 308 + , C.px_4 309 + 310 + -- 311 + , C.md__px_8 312 + , C.lg__px_16 313 + 314 + -- 315 + , ifThenElse justifyCenter C.justify_center "" 316 + ] 317 + nodes 318 + ] 319 + 320 + 321 + inputFocusDecoder : Json.Decode.Decoder Msg 322 + inputFocusDecoder = 323 + Json.Decode.string 324 + |> Json.Decode.at [ "target", "tagName" ] 325 + |> Json.Decode.andThen 326 + (\targetTagName -> 327 + case targetTagName of 328 + "INPUT" -> 329 + interfaceEventHandler Interface.FocusedOnInput 330 + 331 + "TEXTAREA" -> 332 + interfaceEventHandler Interface.FocusedOnInput 333 + 334 + _ -> 335 + Json.Decode.fail "NOT_INPUT" 336 + ) 337 + 338 + 339 + interfaceEventHandler : Interface.Msg -> Json.Decode.Decoder UI.Msg 340 + interfaceEventHandler = 341 + Interface >> Json.Decode.succeed 342 + 343 + 344 + loadingAnimation : Html msg 345 + loadingAnimation = 346 + Html.map never UI.Svg.Elements.loading 347 + 348 + 349 + overlay : Maybe (Alfred Reply) -> Maybe (ContextMenu Reply) -> Html Msg 350 + overlay maybeAlfred maybeContextMenu = 351 + let 352 + isShown = 353 + Maybe.isJust maybeAlfred || Maybe.isJust maybeContextMenu 354 + in 355 + brick 356 + [ onClick (Interface Interface.HideOverlay) ] 357 + [ C.inset_0 358 + , C.bg_black 359 + , C.fixed 360 + , C.transition_1000 361 + , C.transition_ease 362 + , C.transition_opacity 363 + , C.z_30 364 + 365 + -- 366 + , ifThenElse isShown "" C.pointer_events_none 367 + , ifThenElse isShown C.opacity_40 C.opacity_0 368 + ] 369 + [] 370 + 371 + 372 + vessel : List (Html Msg) -> Html Msg 373 + vessel = 374 + (>>) 375 + (brick 376 + [ style "-webkit-mask-image" "-webkit-radial-gradient(white, black)" ] 377 + [ C.bg_white 378 + , C.flex 379 + , C.flex_col 380 + , C.flex_grow 381 + , C.overflow_hidden 382 + , C.relative 383 + , C.rounded 384 + 385 + -- Dark mode 386 + ------------ 387 + , C.dark__bg_darkest_hour 388 + ] 389 + ) 390 + (bricky 391 + [ style "min-height" "296px" ] 392 + [ C.flex 393 + , C.flex_grow 394 + , C.rounded 395 + , C.shadow_lg 396 + , C.w_full 397 + 398 + -- 399 + , C.lg__max_w_insulation 400 + , C.lg__min_w_3xl 401 + ] 402 + )