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 )