A music player that connects to your cloud/distributed storage.
at main 16 kB view raw
1module UI.Queue.State exposing (..) 2 3import Coordinates 4import Debouncer.Basic as Debouncer 5import Dict 6import Html.Events.Extra.Mouse as Mouse 7import List.Extra as List 8import Notifications 9import Queue exposing (..) 10import Return exposing (andThen) 11import Return.Ext as Return 12import Tracks exposing (..) 13import UI.Audio.Types exposing (AudioLoadingState(..)) 14import UI.Common.State as Common 15import UI.Ports as Ports 16import UI.Queue.ContextMenu as Queue 17import UI.Queue.Fill as Fill 18import UI.Queue.Types as Queue exposing (..) 19import UI.Types exposing (..) 20import UI.User.State.Export exposing (..) 21 22 23 24-- 📣 25 26 27update : Queue.Msg -> Manager 28update msg = 29 case msg of 30 Clear -> 31 clear 32 33 PreloadNext -> 34 preloadNext 35 36 Reset -> 37 reset 38 39 ResetIgnored -> 40 resetIgnored 41 42 Rewind -> 43 rewind 44 45 Shift -> 46 shift 47 48 Select a -> 49 select a 50 51 ShowFutureMenu a b c -> 52 showFutureMenu a b c 53 54 ShowFutureNavigationMenu a -> 55 showFutureNavigationMenu a 56 57 ShowHistoryMenu a b -> 58 showHistoryMenu a b 59 60 ToggleRepeat -> 61 toggleRepeat 62 63 ToggleShuffle -> 64 toggleShuffle 65 66 ------------------------------------ 67 -- Future 68 ------------------------------------ 69 AddTracks a -> 70 addTracks a 71 72 InjectFirst a b -> 73 injectFirst a b 74 75 InjectLast a b -> 76 injectLast a b 77 78 InjectFirstAndPlay a -> 79 injectFirstAndPlay a 80 81 MoveItemToFirst a -> 82 moveItemToFirst a 83 84 MoveItemToLast a -> 85 moveItemToLast a 86 87 RemoveItem a -> 88 removeItem a 89 90 91 92-- 🛠 93 94 95changeActiveItem : Maybe Item -> Manager 96changeActiveItem maybeItem model = 97 let 98 maybeNowPlaying = 99 Maybe.map 100 (\item -> 101 { coverLoaded = False 102 , duration = Nothing 103 , isPlaying = False 104 , item = item 105 , loadingState = Loading 106 , playbackPosition = 0 107 } 108 ) 109 maybeItem 110 in 111 maybeItem 112 |> Maybe.map (.identifiedTrack >> Tuple.second) 113 |> Maybe.map 114 (Queue.makeEngineItem 115 False 116 model.currentTime 117 model.sources 118 model.cachedTracks 119 (if model.rememberProgress then 120 model.progress 121 122 else 123 Dict.empty 124 ) 125 ) 126 |> Maybe.map insertTrack 127 |> Maybe.withDefault Return.singleton 128 |> (\fn -> fn { model | nowPlaying = maybeNowPlaying }) 129 |> andThen fill 130 131 132clear : Manager 133clear model = 134 fill { model | playingNext = [] } 135 136 137fill : Manager 138fill model = 139 let 140 ( availableTracks, timestamp ) = 141 ( case ( model.selectedCover, model.coverSelectionReducesPool ) of 142 ( Just cover, True ) -> 143 Tuple.first <| List.foldl coverTracksHarvester ( [], cover.trackIds ) model.tracks.harvested 144 145 _ -> 146 model.tracks.harvested 147 -- 148 , model.currentTime 149 ) 150 151 nonMissingTracks = 152 List.filter 153 (Tuple.second >> .id >> (/=) Tracks.missingId) 154 availableTracks 155 in 156 model 157 |> (\m -> 158 -- Empty the ignored list when we are ignoring all the tracks 159 if List.length model.dontPlay == List.length nonMissingTracks then 160 { m | dontPlay = [] } 161 162 else 163 m 164 ) 165 |> (\m -> 166 if m.shuffle && List.length model.playingNext >= Fill.queueLength then 167 m 168 169 else 170 let 171 fillState = 172 { activeItem = Maybe.map .item m.nowPlaying 173 , future = m.playingNext 174 , ignored = m.dontPlay 175 , past = m.playedPreviously 176 } 177 in 178 -- Fill using the appropiate method 179 if m.shuffle then 180 { m | playingNext = Fill.shuffled timestamp nonMissingTracks fillState } 181 182 else 183 { m | playingNext = Fill.ordered timestamp nonMissingTracks fillState } 184 ) 185 |> Return.communicate 186 (Queue.PreloadNext 187 |> QueueMsg 188 |> Debouncer.provideInput 189 |> AudioPreloadDebounce 190 |> Return.task 191 ) 192 193 194insertTrack : EngineItem -> Manager 195insertTrack item model = 196 item 197 |> (\engineItem -> 198 if 199 List.any 200 (\a -> engineItem.trackId == a.trackId) 201 model.audioElements 202 then 203 List.map 204 (\a -> 205 if engineItem.trackId == a.trackId then 206 { a | isPreload = False } 207 208 else 209 a 210 ) 211 model.audioElements 212 213 else 214 model.audioElements ++ [ engineItem ] 215 ) 216 |> List.filter 217 (\a -> 218 if item.isPreload then 219 True 220 221 else if a.trackId /= item.trackId && not a.isPreload then 222 False 223 224 else 225 True 226 ) 227 |> (\a -> { model | audioElements = a }) 228 |> Return.singleton 229 |> Return.effect_ 230 (\m -> 231 Ports.renderAudioElements 232 { items = m.audioElements 233 , play = 234 if item.isPreload then 235 Nothing 236 237 else 238 Just item.trackId 239 , volume = m.eqSettings.volume 240 } 241 ) 242 243 244preloadNext : Manager 245preloadNext model = 246 case List.head model.playingNext of 247 Just item -> 248 item 249 |> .identifiedTrack 250 |> Tuple.second 251 |> Queue.makeEngineItem 252 True 253 model.currentTime 254 model.sources 255 model.cachedTracks 256 (if model.rememberProgress then 257 model.progress 258 259 else 260 Dict.empty 261 ) 262 |> (\engineItem -> 263 insertTrack engineItem model 264 ) 265 266 Nothing -> 267 Return.singleton model 268 269 270rewind : Manager 271rewind model = 272 changeActiveItem 273 (List.last model.playedPreviously) 274 { model 275 | playingNext = 276 model.nowPlaying 277 |> Maybe.map (\{ item } -> item :: model.playingNext) 278 |> Maybe.withDefault model.playingNext 279 , playedPreviously = 280 model.playedPreviously 281 |> List.init 282 |> Maybe.withDefault [] 283 } 284 285 286{-| Renew the queue, meaning that the auto-generated items in the queue are removed and new items are added. 287-} 288reset : Manager 289reset model = 290 model.playingNext 291 |> List.filter (.manualEntry >> (==) True) 292 |> (\f -> { model | playingNext = f }) 293 |> fill 294 295 296resetIgnored : Manager 297resetIgnored model = 298 fill { model | dontPlay = [] } 299 300 301select : Item -> Manager 302select item model = 303 Return.singleton { model | selectedQueueItem = Just item } 304 305 306shift : Manager 307shift model = 308 changeActiveItem 309 (List.head model.playingNext) 310 { model 311 | playingNext = 312 model.playingNext 313 |> List.drop 1 314 , playedPreviously = 315 model.nowPlaying 316 |> Maybe.map (.item >> List.singleton) 317 |> Maybe.map (List.append model.playedPreviously) 318 |> Maybe.withDefault model.playedPreviously 319 } 320 321 322showFutureMenu : Item -> { index : Int } -> Mouse.Event -> Manager 323showFutureMenu item { index } mouseEvent model = 324 mouseEvent.clientPos 325 |> Coordinates.fromTuple 326 |> Queue.futureMenu 327 { cached = model.cachedTracks 328 , cachingInProgress = model.cachingTracksInProgress 329 , itemIndex = index 330 } 331 item 332 |> Just 333 |> (\c -> { model | contextMenu = c }) 334 |> Return.singleton 335 336 337showFutureNavigationMenu : Mouse.Event -> Manager 338showFutureNavigationMenu mouseEvent model = 339 mouseEvent.clientPos 340 |> Coordinates.fromTuple 341 |> Queue.futureNavigationMenu { manualEntries = List.filter .manualEntry model.playingNext } 342 |> Just 343 |> (\c -> { model | contextMenu = c }) 344 |> Return.singleton 345 346 347showHistoryMenu : Item -> Mouse.Event -> Manager 348showHistoryMenu item mouseEvent model = 349 mouseEvent.clientPos 350 |> Coordinates.fromTuple 351 |> Queue.historyMenu 352 { cached = model.cachedTracks 353 , cachingInProgress = model.cachingTracksInProgress 354 } 355 item 356 |> Just 357 |> (\c -> { model | contextMenu = c }) 358 |> Return.singleton 359 360 361toggleRepeat : Manager 362toggleRepeat model = 363 saveEnclosedUserData { model | repeat = not model.repeat } 364 365 366toggleShuffle : Manager 367toggleShuffle model = 368 { model | shuffle = not model.shuffle } 369 |> reset 370 |> andThen saveEnclosedUserData 371 372 373 374-- 🛠 ░░ FUTURE 375 376 377addTracks : { inFront : Bool, tracks : List IdentifiedTrack } -> Manager 378addTracks { inFront, tracks } = 379 (if inFront then 380 injectFirst 381 382 else 383 injectLast 384 ) 385 { showNotification = True } 386 tracks 387 388 389{-| Add an item in front of the queue. 390-} 391injectFirst : { showNotification : Bool } -> List IdentifiedTrack -> Manager 392injectFirst { showNotification } identifiedTracks model = 393 let 394 ( items, tracks ) = 395 ( List.map (makeItem True) identifiedTracks 396 , List.map Tuple.second identifiedTracks 397 ) 398 399 cleanedFuture = 400 List.foldl 401 (.id >> Fill.cleanAutoGenerated model.shuffle) 402 model.playingNext 403 tracks 404 405 notification = 406 case tracks of 407 [ t ] -> 408 ("__" ++ t.tags.title ++ "__ will be played next") 409 |> Notifications.casual 410 411 list -> 412 list 413 |> List.length 414 |> String.fromInt 415 |> (\s -> "__" ++ s ++ " tracks__ will be played next") 416 |> Notifications.casual 417 in 418 { model | playingNext = items ++ cleanedFuture } 419 |> (if showNotification then 420 Common.showNotification notification 421 422 else 423 Return.singleton 424 ) 425 |> andThen fill 426 427 428injectFirstAndPlay : IdentifiedTrack -> Manager 429injectFirstAndPlay identifiedTrack model = 430 model 431 |> injectFirst { showNotification = False } [ identifiedTrack ] 432 |> andThen shift 433 434 435{-| Add an item after the last manual entry 436(ie. after the last injected item). 437-} 438injectLast : { showNotification : Bool } -> List IdentifiedTrack -> Manager 439injectLast { showNotification } identifiedTracks model = 440 let 441 ( items, tracks ) = 442 ( List.map (makeItem True) identifiedTracks 443 , List.map Tuple.second identifiedTracks 444 ) 445 446 cleanedFuture = 447 List.foldl 448 (.id >> Fill.cleanAutoGenerated model.shuffle) 449 model.playingNext 450 tracks 451 452 manualItems = 453 cleanedFuture 454 |> List.filter (.manualEntry >> (==) True) 455 |> List.length 456 457 newFuture = 458 [] 459 ++ List.take manualItems cleanedFuture 460 ++ items 461 ++ List.drop manualItems cleanedFuture 462 463 notification = 464 case tracks of 465 [ t ] -> 466 ("__" ++ t.tags.title ++ "__ was added to the queue") 467 |> Notifications.casual 468 469 list -> 470 list 471 |> List.length 472 |> String.fromInt 473 |> (\s -> "__" ++ s ++ " tracks__ were added to the queue") 474 |> Notifications.casual 475 in 476 { model | playingNext = newFuture } 477 |> (if showNotification then 478 Common.showNotification notification 479 480 else 481 Return.singleton 482 ) 483 |> andThen fill 484 485 486moveItemToFirst : { index : Int } -> Manager 487moveItemToFirst { index } model = 488 model.playingNext 489 |> moveItem { from = index, to = 0, shuffle = model.shuffle } 490 |> (\f -> { model | playingNext = f }) 491 |> fill 492 493 494moveItemToLast : { index : Int } -> Manager 495moveItemToLast { index } model = 496 let 497 to = 498 model.playingNext 499 |> List.filter (.manualEntry >> (==) True) 500 |> List.length 501 in 502 model.playingNext 503 |> moveItem { from = index, to = to, shuffle = model.shuffle } 504 |> (\f -> { model | playingNext = f }) 505 |> fill 506 507 508removeItem : { index : Int, item : Item } -> Manager 509removeItem { index, item } model = 510 let 511 newFuture = 512 List.removeAt index model.playingNext 513 514 newIgnored = 515 if item.manualEntry then 516 model.dontPlay 517 518 else 519 item :: model.dontPlay 520 in 521 fill { model | playingNext = newFuture, dontPlay = newIgnored } 522 523 524 525-- ⚗️ 526 527 528coverTracksHarvester : 529 IdentifiedTrack 530 -> ( List IdentifiedTrack, List String ) 531 -> ( List IdentifiedTrack, List String ) 532coverTracksHarvester ( i, t ) ( acc, coverTrackIds ) = 533 case List.findIndex ((==) t.id) coverTrackIds of 534 Just idx -> 535 ( acc ++ [ ( i, t ) ] 536 , List.removeAt idx coverTrackIds 537 ) 538 539 Nothing -> 540 ( acc 541 , coverTrackIds 542 ) 543 544 545moveItem : { from : Int, to : Int, shuffle : Bool } -> List Item -> List Item 546moveItem { from, to, shuffle } collection = 547 let 548 subjectItem = 549 collection 550 |> List.getAt from 551 |> Maybe.map (\s -> { s | manualEntry = True }) 552 553 fixedTarget = 554 if to > from then 555 to - 1 556 557 else 558 to 559 in 560 collection 561 |> List.removeAt from 562 |> List.indexedFoldr 563 (\idx existingItem acc -> 564 if idx == fixedTarget then 565 case subjectItem of 566 Just itemToInsert -> 567 List.append [ itemToInsert, existingItem ] acc 568 569 Nothing -> 570 existingItem :: acc 571 572 else if idx < fixedTarget then 573 { existingItem | manualEntry = True } :: acc 574 575 else 576 existingItem :: acc 577 ) 578 [] 579 |> (if shuffle then 580 identity 581 582 else 583 List.filter (.manualEntry >> (==) True) 584 )