A music player that connects to your cloud/distributed storage.
at main 1122 lines 32 kB view raw
1module UI.Tracks.State exposing (..) 2 3import Alien 4import Base64 5import Common exposing (..) 6import ContextMenu 7import Coordinates exposing (Coordinates) 8import Debouncer.Basic as Debouncer 9import Dict 10import Html.Events.Extra.Mouse as Mouse 11import InfiniteList 12import Json.Decode as Json 13import Json.Encode 14import Keyboard 15import List.Ext as List 16import List.Extra as List 17import Maybe.Extra as Maybe 18import Notifications 19import Playlists exposing (Playlist) 20import Queue 21import Return exposing (andThen, return) 22import Return.Ext as Return 23import Sources 24import Task.Extra as Task 25import Tracks exposing (..) 26import Tracks.Collection as Collection 27import Tracks.Encoding as Encoding 28import Tracks.Favourites as Favourites 29import UI.Common.State as Common exposing (showNotification) 30import UI.DnD as DnD 31import UI.Page 32import UI.Ports as Ports 33import UI.Queue.State as Queue 34import UI.Theme 35import UI.Tracks.ContextMenu as Tracks 36import UI.Tracks.Covers as Covers 37import UI.Tracks.Types as Tracks exposing (..) 38import UI.Types exposing (Manager, Model, Msg(..)) 39import UI.User.State.Export as User 40import User.Layer exposing (HypaethralData) 41 42 43 44-- 📣 45 46 47update : Tracks.Msg -> Manager 48update msg = 49 case msg of 50 Download a b -> 51 download a b 52 53 DownloadFinished -> 54 downloadFinished 55 56 Harvest -> 57 harvest 58 59 MarkAsSelected a b -> 60 markAsSelected a b 61 62 ScrollToNowPlaying -> 63 scrollToNowPlaying 64 65 SyncTags a -> 66 syncTags a 67 68 ToggleCachedOnly -> 69 toggleCachedOnly 70 71 ToggleCoverSelectionReducesPool -> 72 toggleCoverSelectionReducesPool 73 74 ToggleFavouritesOnly -> 75 toggleFavouritesOnly 76 77 ToggleHideDuplicates -> 78 toggleHideDuplicates 79 80 ----------------------------------------- 81 -- Cache 82 ----------------------------------------- 83 ClearCache -> 84 clearCache 85 86 RemoveFromCache a -> 87 removeFromCache a 88 89 StoreInCache a -> 90 storeInCache a 91 92 StoredInCache a b -> 93 storedInCache a b 94 95 --------- 96 -- Covers 97 --------- 98 GotCachedCover a -> 99 gotCachedCover a 100 101 InsertCoverCache a -> 102 insertCoverCache a 103 104 ----------------------------------------- 105 -- Collection 106 ----------------------------------------- 107 Add a -> 108 add a 109 110 AddFavourites a -> 111 addFavourites a 112 113 Reload a -> 114 reload a 115 116 RemoveByPaths a -> 117 removeByPaths a 118 119 RemoveBySourceId a -> 120 removeBySourceId a 121 122 RemoveFavourites a -> 123 removeFavourites a 124 125 SortBy a -> 126 sortBy a 127 128 ToggleFavourite a -> 129 toggleFavourite a 130 131 ----------------------------------------- 132 -- Groups 133 ----------------------------------------- 134 DisableGrouping -> 135 disableGrouping 136 137 GroupBy a -> 138 groupBy a 139 140 ----------------------------------------- 141 -- Menus 142 ----------------------------------------- 143 ShowCoverMenu a b -> 144 showCoverMenu a b 145 146 ShowCoverMenuWithSmallDelay a b -> 147 showCoverMenuWithDelay a b 148 149 ShowTracksMenu a b c -> 150 showTracksMenu a b c 151 152 ShowTracksMenuWithSmallDelay a b c -> 153 showTracksMenuWithDelay a b c 154 155 ShowViewMenu a b -> 156 showViewMenu a b 157 158 ----------------------------------------- 159 -- Scenes 160 ----------------------------------------- 161 ChangeScene a -> 162 changeScene a 163 164 DeselectCover -> 165 deselectCover 166 167 InfiniteListMsg a -> 168 infiniteListMsg a 169 170 SelectCover a -> 171 selectCover a 172 173 ----------------------------------------- 174 -- Search 175 ----------------------------------------- 176 ClearSearch -> 177 clearSearch 178 179 Search -> 180 search 181 182 SetSearchResults a -> 183 setSearchResults a 184 185 SetSearchTerm a -> 186 setSearchTerm a 187 188 189 190-- 🔱 191 192 193add : Json.Value -> Manager 194add encodedTracks model = 195 reviseCollection 196 (encodedTracks 197 |> Json.decodeValue (Json.list Encoding.trackDecoder) 198 |> Result.withDefault [] 199 |> Collection.add 200 ) 201 model 202 203 204addFavourites : List IdentifiedTrack -> Manager 205addFavourites = 206 manageFavourites AddToFavourites 207 208 209afterInitialLoad : Manager 210afterInitialLoad model = 211 Common.toggleLoadingScreen Off model 212 213 214changeScene : Scene -> Manager 215changeScene scene model = 216 (case scene of 217 Covers -> 218 Ports.loadAlbumCovers { list = True, coverView = True } 219 220 List -> 221 Cmd.none 222 ) 223 |> return { model | scene = scene, selectedCover = Nothing } 224 |> andThen 225 (if model.coverSelectionReducesPool then 226 Queue.reset 227 228 else 229 Return.singleton 230 ) 231 |> andThen Common.forceTracksRerender 232 |> andThen User.saveEnclosedUserData 233 234 235clearCache : Manager 236clearCache model = 237 model.cachedTracks 238 |> Json.Encode.list Json.Encode.string 239 |> Alien.broadcast Alien.RemoveTracksFromCache 240 |> Ports.toBrain 241 |> return { model | cachedTracks = [] } 242 |> andThen harvest 243 |> andThen User.saveEnclosedUserData 244 |> andThen 245 ("Tracks cache was cleared" 246 |> Notifications.casual 247 |> Common.showNotification 248 ) 249 250 251clearSearch : Manager 252clearSearch model = 253 { model | searchResults = Nothing, searchTerm = Nothing } 254 |> reviseCollection Collection.harvest 255 |> andThen User.saveEnclosedUserData 256 257 258deselectCover : Manager 259deselectCover model = 260 (if model.coverSelectionReducesPool then 261 Queue.reset 262 263 else 264 Return.singleton 265 ) 266 { model | selectedCover = Nothing } 267 268 269download : { prefixTrackNumber : Bool, zipName : String } -> List Track -> Manager 270download { prefixTrackNumber, zipName } tracks model = 271 let 272 notification = 273 Notifications.stickyCasual "Downloading tracks ..." 274 275 downloading = 276 Just { notificationId = Notifications.id notification } 277 in 278 [ ( "prefixTrackNumber", Json.Encode.bool prefixTrackNumber ) 279 , ( "trackIds" 280 , tracks 281 |> List.map .id 282 |> Json.Encode.list Json.Encode.string 283 ) 284 , ( "zipName", Json.Encode.string zipName ) 285 ] 286 |> Json.Encode.object 287 |> Alien.broadcast Alien.DownloadTracks 288 |> Ports.toBrain 289 |> return { model | downloading = downloading } 290 |> andThen (Common.showNotification notification) 291 292 293downloadFinished : Manager 294downloadFinished model = 295 case model.downloading of 296 Just { notificationId } -> 297 Common.dismissNotification 298 { id = notificationId } 299 { model | downloading = Nothing } 300 301 Nothing -> 302 Return.singleton model 303 304 305disableGrouping : Manager 306disableGrouping model = 307 { model | grouping = Nothing } 308 |> reviseCollection Collection.arrange 309 |> andThen User.saveEnclosedUserData 310 311 312failedToStoreInCache : List String -> Manager 313failedToStoreInCache trackIds m = 314 showNotification 315 (Notifications.error "Failed to store track in cache") 316 { m | cachingTracksInProgress = List.without trackIds m.cachingTracksInProgress } 317 318 319finishedStoringInCache : List String -> Manager 320finishedStoringInCache trackIds model = 321 { model 322 | cachedTracks = model.cachedTracks ++ trackIds 323 , cachingTracksInProgress = List.without trackIds model.cachingTracksInProgress 324 } 325 |> (\m -> 326 -- When a context menu of a track is open, 327 -- it should be "rerendered" in case 328 -- the track is no longer being downloaded. 329 case m.contextMenu of 330 Just contextMenu -> 331 let 332 isTrackContextMenu = 333 ContextMenu.anyItem 334 (.label >> (==) "Downloading ...") 335 contextMenu 336 337 coordinates = 338 ContextMenu.coordinates contextMenu 339 in 340 if isTrackContextMenu then 341 showTracksMenu Nothing { alt = False } coordinates m 342 343 else 344 Return.singleton m 345 346 Nothing -> 347 Return.singleton m 348 ) 349 |> andThen harvest 350 |> andThen User.saveEnclosedUserData 351 352 353generateCovers : Manager 354generateCovers model = 355 model.tracks 356 |> Covers.generate model.sortBy 357 |> (\c -> { model | covers = c }) 358 |> Return.singleton 359 360 361gotCachedCover : Json.Value -> Manager 362gotCachedCover json model = 363 let 364 cachedCovers = 365 Maybe.withDefault Dict.empty model.cachedCovers 366 367 decodedValue = 368 Json.decodeValue 369 (Json.map3 370 (\i k u -> ( i, k, u )) 371 (Json.field "imageType" Json.string) 372 (Json.field "key" Json.string) 373 (Json.field "url" Json.string) 374 ) 375 json 376 in 377 decodedValue 378 |> Result.map (\( _, key, url ) -> Dict.insert key url cachedCovers) 379 |> Result.map (\dict -> { model | cachedCovers = Just dict }) 380 |> Result.withDefault model 381 |> (\m -> 382 case ( m.nowPlaying, decodedValue ) of 383 ( Just nowPlaying, Ok val ) -> 384 let 385 ( imageType, key, url ) = 386 val 387 388 ( _, track ) = 389 nowPlaying.item.identifiedTrack 390 391 hasntLoadedYet = 392 nowPlaying.coverLoaded == False 393 394 ( keyA, keyB ) = 395 ( Base64.encode (Tracks.coverKey False track) 396 , Base64.encode (Tracks.coverKey True track) 397 ) 398 399 keyMatches = 400 keyA == key || keyB == key 401 in 402 if hasntLoadedYet && keyMatches then 403 ( m, Ports.setMediaSessionArtwork { blobUrl = url, imageType = imageType } ) 404 405 else 406 Return.singleton m 407 408 _ -> 409 Return.singleton m 410 ) 411 412 413groupBy : Tracks.Grouping -> Manager 414groupBy grouping model = 415 { model | grouping = Just grouping } 416 |> reviseCollection Collection.arrange 417 |> andThen User.saveEnclosedUserData 418 419 420harvest : Manager 421harvest = 422 reviseCollection Collection.harvest 423 424 425harvestCovers : Manager 426harvestCovers model = 427 model.covers 428 |> Covers.harvest model.selectedCover model.sortBy model.tracks 429 |> (\( c, s ) -> { model | covers = c, selectedCover = s }) 430 |> Return.communicate (Ports.loadAlbumCovers { list = True, coverView = True }) 431 432 433infiniteListMsg : InfiniteList.Model -> Manager 434infiniteListMsg infiniteList model = 435 return 436 { model | infiniteList = infiniteList } 437 (Ports.loadAlbumCovers { list = True, coverView = False }) 438 439 440insertCoverCache : Json.Value -> Manager 441insertCoverCache json model = 442 json 443 |> Json.decodeValue (Json.dict Json.string) 444 |> Result.map (\dict -> { model | cachedCovers = Just dict }) 445 |> Result.withDefault model 446 |> Return.singleton 447 448 449manageFavourites : FavouritesManagementAction -> List IdentifiedTrack -> Manager 450manageFavourites action tracks model = 451 let 452 newFavourites = 453 (case action of 454 AddToFavourites -> 455 Favourites.completeFavouritesList 456 457 RemoveFromFavourites -> 458 Favourites.removeFromFavouritesList 459 ) 460 tracks 461 model.favourites 462 463 effect collection = 464 collection 465 |> Collection.map 466 (case action of 467 AddToFavourites -> 468 Favourites.completeTracksList tracks 469 470 RemoveFromFavourites -> 471 Favourites.removeFromTracksList tracks 472 ) 473 |> (if model.favouritesOnly then 474 Collection.harvest 475 476 else 477 identity 478 ) 479 480 selectedCover = 481 Maybe.map 482 (\cover -> 483 cover.tracks 484 |> (case action of 485 AddToFavourites -> 486 Favourites.completeTracksList tracks 487 488 RemoveFromFavourites -> 489 Favourites.removeFromTracksList tracks 490 ) 491 |> (\a -> { cover | tracks = a }) 492 ) 493 model.selectedCover 494 in 495 { model | favourites = newFavourites, selectedCover = selectedCover } 496 |> reviseCollection effect 497 |> andThen User.saveFavourites 498 |> (if model.scene == Covers then 499 andThen generateCovers >> andThen harvestCovers 500 501 else 502 identity 503 ) 504 505 506markAsSelected : Int -> { shiftKey : Bool } -> Manager 507markAsSelected indexInList { shiftKey } model = 508 let 509 selection = 510 if shiftKey then 511 model.selectedTrackIndexes 512 |> List.head 513 |> Maybe.map 514 (\n -> 515 if n > indexInList then 516 List.range indexInList n 517 518 else 519 List.range n indexInList 520 ) 521 |> Maybe.withDefault [ indexInList ] 522 523 else 524 [ indexInList ] 525 in 526 Return.singleton { model | selectedTrackIndexes = selection } 527 528 529reload : Json.Value -> Manager 530reload encodedTracks model = 531 reviseCollection 532 (encodedTracks 533 |> Json.decodeValue (Json.list Encoding.trackDecoder) 534 |> Result.withDefault model.tracks.untouched 535 |> Collection.replace 536 ) 537 model 538 539 540removeByPaths : Json.Value -> Manager 541removeByPaths encodedParams model = 542 let 543 decoder = 544 Json.map2 545 Tuple.pair 546 (Json.field "filePaths" <| Json.list Json.string) 547 (Json.field "sourceId" Json.string) 548 549 ( paths, sourceId ) = 550 encodedParams 551 |> Json.decodeValue decoder 552 |> Result.withDefault ( [], missingId ) 553 554 { kept, removed } = 555 Tracks.removeByPaths 556 { sourceId = sourceId, paths = paths } 557 model.tracks.untouched 558 559 newCollection = 560 { emptyCollection | untouched = kept } 561 in 562 { model | tracks = newCollection } 563 |> reviseCollection Collection.identify 564 |> andThen (removeFromCache removed) 565 566 567removeBySourceId : String -> Manager 568removeBySourceId sourceId model = 569 let 570 { kept, removed } = 571 Tracks.removeBySourceId sourceId model.tracks.untouched 572 573 newCollection = 574 { emptyCollection | untouched = kept } 575 in 576 sourceId 577 |> Json.Encode.string 578 |> Alien.broadcast Alien.RemoveTracksBySourceId 579 |> Ports.toBrain 580 |> return { model | tracks = newCollection } 581 |> andThen (reviseCollection Collection.identify) 582 |> andThen (removeFromCache removed) 583 584 585removeFavourites : List IdentifiedTrack -> Manager 586removeFavourites = 587 manageFavourites RemoveFromFavourites 588 589 590removeFromCache : List Track -> Manager 591removeFromCache tracks model = 592 let 593 trackIds = 594 List.map .id tracks 595 in 596 trackIds 597 |> Json.Encode.list Json.Encode.string 598 |> Alien.broadcast Alien.RemoveTracksFromCache 599 |> Ports.toBrain 600 |> return { model | cachedTracks = List.without trackIds model.cachedTracks } 601 |> andThen harvest 602 |> andThen User.saveEnclosedUserData 603 604 605reviseCollection : (Parcel -> Parcel) -> Manager 606reviseCollection collector model = 607 resolveParcel 608 (model 609 |> makeParcel 610 |> collector 611 ) 612 model 613 614 615search : Manager 616search model = 617 case ( model.searchTerm, model.searchResults ) of 618 ( Just term, _ ) -> 619 term 620 |> String.trim 621 |> Json.Encode.string 622 |> Ports.giveBrain Alien.SearchTracks 623 |> return model 624 625 ( Nothing, Just _ ) -> 626 reviseCollection Collection.harvest { model | searchResults = Nothing } 627 628 ( Nothing, Nothing ) -> 629 Return.singleton model 630 631 632selectCover : Cover -> Manager 633selectCover cover model = 634 { model | selectedCover = Just cover } 635 |> (if model.coverSelectionReducesPool then 636 Queue.reset 637 638 else 639 Return.singleton 640 ) 641 |> Return.command (Ports.loadAlbumCovers { list = False, coverView = True }) 642 643 644setSearchResults : Json.Value -> Manager 645setSearchResults json model = 646 case model.searchTerm of 647 Just _ -> 648 json 649 |> Json.decodeValue (Json.list Json.string) 650 |> Result.withDefault [] 651 |> (\results -> { model | searchResults = Just results }) 652 |> reviseCollection Collection.harvest 653 |> andThen afterInitialLoad 654 655 Nothing -> 656 Return.singleton model 657 658 659setSearchTerm : String -> Manager 660setSearchTerm term model = 661 (case String.trim term of 662 "" -> 663 { model | searchTerm = Nothing } 664 665 _ -> 666 { model | searchTerm = Just term } 667 ) 668 |> Return.communicate 669 (Search 670 |> TracksMsg 671 |> Debouncer.provideInput 672 |> SearchDebounce 673 |> Task.do 674 ) 675 |> Return.andThen User.saveEnclosedUserData 676 677 678showCoverMenu : Cover -> Coordinates -> Manager 679showCoverMenu cover coordinates model = 680 let 681 menuDependencies = 682 { cached = model.cachedTracks 683 , cachingInProgress = model.cachingTracksInProgress 684 , currentTime = model.currentTime 685 , selectedPlaylist = model.selectedPlaylist 686 , lastModifiedPlaylistName = model.lastModifiedPlaylist 687 , showAlternativeMenu = False 688 , sources = model.sources 689 } 690 in 691 coordinates 692 |> Tracks.trackMenu menuDependencies cover.tracks 693 |> Common.showContextMenuWithModel model 694 695 696showCoverMenuWithDelay : Cover -> Coordinates -> Manager 697showCoverMenuWithDelay a b model = 698 Tracks.ShowCoverMenu a b 699 |> TracksMsg 700 |> Task.doDelayed 250 701 |> return model 702 703 704showTracksMenu : Maybe Int -> { alt : Bool } -> Coordinates -> Manager 705showTracksMenu maybeTrackIndex { alt } coordinates model = 706 let 707 selection = 708 case maybeTrackIndex of 709 Just trackIndex -> 710 if List.isEmpty model.selectedTrackIndexes then 711 [ trackIndex ] 712 713 else if List.member trackIndex model.selectedTrackIndexes == False then 714 [ trackIndex ] 715 716 else 717 model.selectedTrackIndexes 718 719 Nothing -> 720 model.selectedTrackIndexes 721 722 menuDependencies = 723 { cached = model.cachedTracks 724 , cachingInProgress = model.cachingTracksInProgress 725 , currentTime = model.currentTime 726 , selectedPlaylist = model.selectedPlaylist 727 , lastModifiedPlaylistName = model.lastModifiedPlaylist 728 , showAlternativeMenu = alt 729 , sources = model.sources 730 } 731 732 tracks = 733 List.pickIndexes selection model.tracks.harvested 734 in 735 coordinates 736 |> Tracks.trackMenu menuDependencies tracks 737 |> Common.showContextMenuWithModel 738 { model 739 | dnd = DnD.initialModel 740 , selectedTrackIndexes = selection 741 } 742 743 744showTracksMenuWithDelay : Maybe Int -> { alt : Bool } -> Coordinates -> Manager 745showTracksMenuWithDelay a b c model = 746 Tracks.ShowTracksMenu a b c 747 |> TracksMsg 748 |> Task.doDelayed 250 749 |> return model 750 751 752showViewMenu : Maybe Grouping -> Mouse.Event -> Manager 753showViewMenu maybeGrouping mouseEvent model = 754 mouseEvent.clientPos 755 |> Coordinates.fromTuple 756 |> Tracks.viewMenu model.cachedTracksOnly maybeGrouping 757 |> Common.showContextMenuWithModel model 758 759 760scrollToNowPlaying : Manager 761scrollToNowPlaying model = 762 model.nowPlaying 763 |> Maybe.map 764 (.item >> .identifiedTrack >> Tuple.second >> .id) 765 |> Maybe.andThen 766 (\id -> 767 List.find 768 (Tuple.second >> .id >> (==) id) 769 model.tracks.harvested 770 ) 771 |> Maybe.map 772 (\( identifiers, track ) -> 773 case model.scene of 774 Covers -> 775 if List.member Keyboard.Shift model.pressedKeys then 776 return 777 { model | selectedCover = Nothing } 778 (UI.Theme.scrollToNowPlaying Covers ( identifiers, track ) model) 779 780 else 781 model.covers.harvested 782 |> List.find (\cover -> List.member track.id cover.trackIds) 783 |> Maybe.unwrap model (\cover -> { model | selectedCover = Just cover }) 784 |> Return.communicate (Ports.loadAlbumCovers { list = True, coverView = True }) 785 786 List -> 787 return 788 { model | selectedCover = Nothing } 789 (UI.Theme.scrollToNowPlaying List ( identifiers, track ) model) 790 ) 791 |> Maybe.map 792 (UI.Page.Index 793 |> Common.changeUrlUsingPage 794 |> andThen 795 ) 796 |> Maybe.withDefault 797 (Return.singleton model) 798 799 800sortBy : SortBy -> Manager 801sortBy property model = 802 let 803 sortDir = 804 if model.sortBy /= property then 805 Asc 806 807 else if model.sortDirection == Asc then 808 Desc 809 810 else 811 Asc 812 in 813 { model | sortBy = property, sortDirection = sortDir } 814 |> reviseCollection Collection.arrange 815 |> andThen User.saveEnclosedUserData 816 817 818storeInCache : List Track -> Manager 819storeInCache tracks model = 820 let 821 trackIds = 822 List.map .id tracks 823 824 notification = 825 case tracks of 826 [ t ] -> 827 ("__" ++ t.tags.title ++ "__ will be stored in the cache") 828 |> Notifications.casual 829 830 list -> 831 list 832 |> List.length 833 |> String.fromInt 834 |> (\s -> "__" ++ s ++ " tracks__ will be stored in the cache") 835 |> Notifications.casual 836 in 837 tracks 838 |> Json.Encode.list 839 (\track -> 840 Json.Encode.object 841 [ ( "trackId" 842 , Json.Encode.string track.id 843 ) 844 , ( "url" 845 , track 846 |> Queue.makeTrackUrl 847 model.currentTime 848 model.sources 849 |> Json.Encode.string 850 ) 851 ] 852 ) 853 |> Alien.broadcast Alien.StoreTracksInCache 854 |> Ports.toBrain 855 |> return { model | cachingTracksInProgress = model.cachingTracksInProgress ++ trackIds } 856 |> andThen (Common.showNotification notification) 857 858 859storedInCache : Json.Value -> Maybe String -> Manager 860storedInCache json maybeError = 861 case 862 ( maybeError 863 , Json.decodeValue (Json.list Json.string) json 864 ) 865 of 866 ( Nothing, Ok list ) -> 867 finishedStoringInCache list 868 869 ( Nothing, Err err ) -> 870 err 871 |> Json.errorToString 872 |> Notifications.error 873 |> Common.showNotification 874 875 ( Just _, Ok trackIds ) -> 876 failedToStoreInCache trackIds 877 878 ( Just err, Err _ ) -> 879 err 880 |> Notifications.error 881 |> Common.showNotification 882 883 884syncTags : List Track -> Manager 885syncTags tracks = 886 tracks 887 |> Json.Encode.list 888 (\track -> 889 Json.Encode.object 890 [ ( "path", Json.Encode.string track.path ) 891 , ( "sourceId", Json.Encode.string track.sourceId ) 892 , ( "trackId", Json.Encode.string track.id ) 893 ] 894 ) 895 |> Alien.broadcast Alien.SyncTrackTags 896 |> Ports.toBrain 897 |> Return.communicate 898 899 900toggleCachedOnly : Manager 901toggleCachedOnly model = 902 { model | cachedTracksOnly = not model.cachedTracksOnly } 903 |> reviseCollection Collection.harvest 904 |> andThen User.saveEnclosedUserData 905 |> andThen Common.forceTracksRerender 906 907 908toggleCoverSelectionReducesPool : Manager 909toggleCoverSelectionReducesPool model = 910 { model | coverSelectionReducesPool = not model.coverSelectionReducesPool } 911 |> Queue.reset 912 |> andThen User.saveSettings 913 914 915toggleFavourite : Int -> Manager 916toggleFavourite index model = 917 case List.getAt index model.tracks.harvested of 918 Just ( i, t ) -> 919 let 920 newFavourites = 921 Favourites.toggleInFavouritesList ( i, t ) model.favourites 922 923 effect collection = 924 collection 925 |> Collection.map (Favourites.toggleInTracksList t) 926 |> (if model.favouritesOnly then 927 Collection.harvest 928 929 else 930 identity 931 ) 932 933 selectedCover = 934 Maybe.map 935 (\cover -> 936 cover.tracks 937 |> Favourites.toggleInTracksList t 938 |> (\a -> { cover | tracks = a }) 939 ) 940 model.selectedCover 941 in 942 { model | favourites = newFavourites, selectedCover = selectedCover } 943 |> reviseCollection effect 944 |> andThen User.saveFavourites 945 |> (if model.scene == Covers then 946 andThen generateCovers >> andThen harvestCovers 947 948 else 949 identity 950 ) 951 952 Nothing -> 953 Return.singleton model 954 955 956toggleFavouritesOnly : Manager 957toggleFavouritesOnly model = 958 { model | favouritesOnly = not model.favouritesOnly } 959 |> reviseCollection Collection.harvest 960 |> andThen User.saveEnclosedUserData 961 962 963toggleHideDuplicates : Manager 964toggleHideDuplicates model = 965 { model | hideDuplicates = not model.hideDuplicates } 966 |> reviseCollection Collection.arrange 967 |> andThen User.saveSettings 968 969 970 971-- 📣 ░░ PARCEL 972 973 974makeParcel : Model -> Parcel 975makeParcel model = 976 ( { cached = model.cachedTracks 977 , cachedOnly = model.cachedTracksOnly 978 , enabledSourceIds = Sources.enabledSourceIds model.sources 979 , favourites = model.favourites 980 , favouritesOnly = model.favouritesOnly 981 , grouping = model.grouping 982 , hideDuplicates = model.hideDuplicates 983 , searchResults = model.searchResults 984 , selectedPlaylist = model.selectedPlaylist 985 , sortBy = model.sortBy 986 , sortDirection = model.sortDirection 987 } 988 , model.tracks 989 ) 990 991 992resolveParcel : Parcel -> Manager 993resolveParcel ( deps, newCollection ) model = 994 let 995 scrollObj = 996 Json.Encode.object 997 [ ( "scrollTop", Json.Encode.int 0 ) ] 998 999 scrollEvent = 1000 Json.Encode.object 1001 [ ( "target", scrollObj ) ] 1002 1003 newScrollContext = 1004 scrollContext model 1005 1006 collectionChanged = 1007 Collection.tracksChanged 1008 model.tracks.untouched 1009 newCollection.untouched 1010 1011 arrangementChanged = 1012 if collectionChanged then 1013 True 1014 1015 else 1016 Collection.identifiedTracksChanged 1017 model.tracks.arranged 1018 newCollection.arranged 1019 1020 harvestChanged = 1021 if arrangementChanged then 1022 True 1023 1024 else 1025 Collection.identifiedTracksChanged 1026 model.tracks.harvested 1027 newCollection.harvested 1028 1029 scrollContextChanged = 1030 newScrollContext /= model.tracks.scrollContext 1031 1032 modelWithNewCollection = 1033 (if model.scene == List && scrollContextChanged then 1034 \m -> { m | infiniteList = InfiniteList.updateScroll scrollEvent m.infiniteList } 1035 1036 else 1037 identity 1038 ) 1039 { model 1040 | tracks = 1041 { newCollection | scrollContext = newScrollContext } 1042 , selectedTrackIndexes = 1043 if collectionChanged || harvestChanged then 1044 [] 1045 1046 else 1047 model.selectedTrackIndexes 1048 } 1049 in 1050 (if collectionChanged then 1051 whenCollectionChanges 1052 1053 else if arrangementChanged then 1054 whenArrangementChanges 1055 1056 else if harvestChanged then 1057 whenHarvestChanges 1058 1059 else 1060 identity 1061 ) 1062 ( modelWithNewCollection 1063 ----------------------------------------- 1064 -- Command 1065 ----------------------------------------- 1066 , if scrollContextChanged then 1067 UI.Theme.scrollTracksToTop model.scene 1068 1069 else 1070 Cmd.none 1071 ) 1072 1073 1074whenHarvestChanges = 1075 andThen harvestCovers >> andThen Queue.reset 1076 1077 1078whenArrangementChanges = 1079 andThen generateCovers >> whenHarvestChanges 1080 1081 1082whenCollectionChanges = 1083 andThen search >> andThen Common.generateDirectoryPlaylists >> whenArrangementChanges 1084 1085 1086scrollContext : Model -> String 1087scrollContext model = 1088 String.concat 1089 [ Maybe.withDefault "" <| model.searchTerm 1090 , Maybe.withDefault "" <| Maybe.map .name model.selectedPlaylist 1091 ] 1092 1093 1094 1095-- 📣 ░░ USER DATA 1096 1097 1098importHypaethral : HypaethralData -> Maybe Playlist -> Manager 1099importHypaethral data selectedPlaylist model = 1100 { model 1101 | favourites = data.favourites 1102 , selectedPlaylist = selectedPlaylist 1103 , tracks = { emptyCollection | untouched = data.tracks } 1104 } 1105 |> reviseCollection Collection.identify 1106 |> andThen search 1107 |> (case model.searchTerm of 1108 Just _ -> 1109 identity 1110 1111 Nothing -> 1112 andThen afterInitialLoad 1113 ) 1114 1115 1116 1117-- ㊙️ 1118 1119 1120type FavouritesManagementAction 1121 = AddToFavourites 1122 | RemoveFromFavourites