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

Configure Feed

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

Refactor queue state

+914 -879
+28 -16
src/Applications/UI.elm
··· 32 32 import UI.Authentication.State as Authentication 33 33 import UI.Backdrop as Backdrop 34 34 import UI.Common.State as Common 35 + import UI.DnD as DnD 35 36 import UI.Equalizer.State as Equalizer 36 37 import UI.Equalizer.View as Equalizer 37 38 import UI.Interface.State as Interface ··· 40 41 import UI.Playlists.ContextMenu as Playlists 41 42 import UI.Playlists.State as Playlists 42 43 import UI.Ports as Ports 43 - import UI.Queue as Queue 44 - import UI.Queue.ContextMenu as Queue 44 + import UI.Queue.State as Queue 45 + import UI.Queue.Types as Queue 45 46 import UI.Reply.Translate as Reply 46 47 import UI.Routing.State as Routing 47 48 import UI.Services.State as Services ··· 94 95 , currentTime = Time.millisToPosix flags.initialTime 95 96 , darkMode = flags.darkMode 96 97 , downloading = Nothing 98 + , dnd = DnD.initialModel 97 99 , focusedOnInput = False 98 100 , isDragging = False 99 101 , isLoading = True ··· 159 161 , playlistToActivate = Nothing 160 162 161 163 ----------------------------------------- 162 - -- Children (TODO) 164 + -- Queue 165 + ----------------------------------------- 166 + , dontPlay = [] 167 + , nowPlaying = Nothing 168 + , playedPreviously = [] 169 + , playingNext = [] 170 + , selectedQueueItem = Nothing 171 + 172 + -- 173 + , repeat = False 174 + , shuffle = False 175 + 176 + ----------------------------------------- 177 + -- 🦉 Nested 163 178 ----------------------------------------- 164 179 , authentication = Authentication.initialModel url 165 - , queue = Queue.initialModel 180 + 181 + ----------------------------------------- 182 + -- Children (TODO) 183 + ----------------------------------------- 166 184 , sources = Sources.initialModel 167 185 , tracks = Tracks.initialModel 168 186 } ··· 279 297 280 298 Debounce a -> 281 299 Interface.debounce update a 300 + 301 + DnD a -> 302 + Interface.dnd a 282 303 283 304 FocusedOnInput -> 284 305 Interface.focusedOnInput ··· 415 436 AuthenticationMsg a -> 416 437 Authentication.update a 417 438 439 + QueueMsg a -> 440 + Queue.update a 441 + 418 442 ----------------------------------------- 419 443 -- Children (TODO) 420 444 ----------------------------------------- 421 - QueueMsg sub -> 422 - \model -> 423 - Return3.wieldNested 424 - Reply.translate 425 - { mapCmd = QueueMsg 426 - , mapModel = \child -> { model | queue = child } 427 - , update = Queue.update 428 - } 429 - { model = model.queue 430 - , msg = sub 431 - } 432 - 433 445 SourcesMsg sub -> 434 446 \model -> 435 447 Return3.wieldNested
+3 -4
src/Applications/UI/Audio/State.elm
··· 6 6 import Return exposing (return) 7 7 import Return.Ext as Return exposing (communicate) 8 8 import UI.Ports as Ports 9 - import UI.Queue as Queue 9 + import UI.Queue.State as Queue 10 10 import UI.Reply as Reply 11 11 import UI.Types as UI exposing (Manager, Organizer) 12 12 ··· 40 40 41 41 playPause : Manager 42 42 playPause model = 43 - if Maybe.isNothing model.queue.activeItem then 44 - -- TODO! 45 - Return.performance (UI.QueueMsg Queue.Shift) model 43 + if Maybe.isNothing model.nowPlaying then 44 + Queue.shift model 46 45 47 46 else if model.audioIsPlaying then 48 47 communicate (Ports.pause ()) model
-1
src/Applications/UI/Console.elm
··· 12 12 import Material.Icons.Types exposing (Coloring(..)) 13 13 import Maybe.Extra as Maybe 14 14 import Queue 15 - import UI.Queue as Queue 16 15 import UI.Reply exposing (Reply(..)) 17 16 18 17
+7 -4
src/Applications/UI/DnD.elm
··· 3 3 import Html exposing (Attribute) 4 4 import Html.Events.Extra.Mouse as Mouse 5 5 import Html.Events.Extra.Pointer as Pointer 6 - import UI.Reply as Reply exposing (Reply) 7 6 8 7 9 8 ··· 30 29 } 31 30 32 31 32 + type alias Response = 33 + { initiated : Bool } 34 + 35 + 33 36 initialModel : Model context 34 37 initialModel = 35 38 NotDragging ··· 39 42 -- 📣 40 43 41 44 42 - update : Msg context -> Model context -> ( Model context, List Reply ) 45 + update : Msg context -> Model context -> ( Model context, Response ) 43 46 update msg model = 44 47 ( ------------------------------------ 45 48 -- Model ··· 96 99 ------------------------------------ 97 100 , case msg of 98 101 Start context -> 99 - [ Reply.StartedDragging ] 102 + { initiated = True } 100 103 101 104 _ -> 102 - [] 105 + { initiated = False } 103 106 ) 104 107 105 108
+39 -8
src/Applications/UI/Interface/State.elm
··· 7 7 import UI.Common.State as Common exposing (modifySingleton) 8 8 import UI.DnD as DnD 9 9 import UI.Page as Page 10 - import UI.Queue as Queue 11 10 import UI.Queue.State as Queue 12 11 import UI.Tracks as Tracks 13 12 import UI.Tracks.Scene.List ··· 47 46 return updatedModel mappedCmd 48 47 49 48 49 + dnd : DnD.Msg Int -> Manager 50 + dnd dragMsg model = 51 + let 52 + ( d, { initiated } ) = 53 + DnD.update dragMsg model.dnd 54 + 55 + m = 56 + if initiated then 57 + { model | dnd = d, isDragging = True } 58 + 59 + else 60 + { model | dnd = d } 61 + in 62 + if DnD.hasDropped d then 63 + case model.page of 64 + Page.Queue _ -> 65 + let 66 + ( from, to ) = 67 + ( Maybe.withDefault 0 <| DnD.modelSubject d 68 + , Maybe.withDefault 0 <| DnD.modelTarget d 69 + ) 70 + 71 + newFuture = 72 + Queue.moveItem 73 + { from = from, to = to, shuffle = model.shuffle } 74 + model.playingNext 75 + in 76 + Queue.fill { m | playingNext = newFuture } 77 + 78 + _ -> 79 + Return.singleton m 80 + 81 + else 82 + Return.singleton m 83 + 84 + 50 85 focusedOnInput : Manager 51 86 focusedOnInput model = 52 87 Return.singleton { model | focusedOnInput = True } ··· 68 103 69 104 70 105 removeQueueSelection : Manager 71 - removeQueueSelection = 72 - modifySingleton Queue.lens (\q -> { q | selection = Nothing }) 106 + removeQueueSelection model = 107 + Return.singleton { model | selectedQueueItem = Nothing } 73 108 74 109 75 110 removeTrackSelection : Manager ··· 101 136 -- do the appropriate thing. 102 137 case model.page of 103 138 Page.Queue _ -> 104 - -- TODO! 105 - DnD.stoppedDragging 106 - |> Queue.DragMsg 107 - |> QueueMsg 108 - |> Return.performanceF notDragging 139 + dnd DnD.stoppedDragging notDragging 109 140 110 141 Page.Index -> 111 142 case model.tracks.scene of
-730
src/Applications/UI/Queue.elm
··· 1 - module UI.Queue exposing (Model, Msg(..), initialModel, moveQueueItemToFirst, moveQueueItemToLast, update, view) 2 - 3 - import Chunky exposing (..) 4 - import Common 5 - import Conditional exposing (..) 6 - import Coordinates 7 - import Css.Classes as C 8 - import Html exposing (Html, text) 9 - import Html.Attributes exposing (href) 10 - import Html.Events.Extra.Mouse as Mouse 11 - import Icons 12 - import List.Extra as List 13 - import Material.Icons as Icons 14 - import Material.Icons.Types exposing (Coloring(..)) 15 - import Queue exposing (..) 16 - import Return3 exposing (..) 17 - import Time 18 - import Tracks exposing (IdentifiedTrack) 19 - import UI.DnD as DnD 20 - import UI.Kit 21 - import UI.List 22 - import UI.Navigation exposing (..) 23 - import UI.Page as Page 24 - import UI.Ports as Ports 25 - import UI.Queue.Fill as Fill 26 - import UI.Queue.Page as Queue exposing (Page(..)) 27 - import UI.Reply exposing (Reply(..)) 28 - import UI.Sources.Page 29 - 30 - 31 - 32 - -- 🌳 33 - 34 - 35 - type alias Model = 36 - { activeItem : Maybe Item 37 - , future : List Item 38 - , ignored : List Item 39 - , past : List Item 40 - 41 - -- 42 - , repeat : Bool 43 - , shuffle : Bool 44 - 45 - -- 46 - , dnd : DnD.Model Int 47 - , selection : Maybe Item 48 - } 49 - 50 - 51 - initialModel : Model 52 - initialModel = 53 - { activeItem = Nothing 54 - , future = [] 55 - , ignored = [] 56 - , past = [] 57 - 58 - -- 59 - , repeat = False 60 - , shuffle = False 61 - 62 - -- 63 - , dnd = DnD.initialModel 64 - , selection = Nothing 65 - } 66 - 67 - 68 - 69 - -- 📣 70 - 71 - 72 - type Msg 73 - = Select Item 74 - | ShowFutureMenu Item { index : Int } Mouse.Event 75 - | ShowHistoryMenu Item Mouse.Event 76 - ------------------------------------ 77 - -- Combos 78 - ------------------------------------ 79 - | InjectFirstAndPlay IdentifiedTrack 80 - ------------------------------------ 81 - -- Future 82 - ------------------------------------ 83 - | InjectFirst { showNotification : Bool } (List IdentifiedTrack) 84 - | InjectLast { showNotification : Bool } (List IdentifiedTrack) 85 - | RemoveItem { index : Int, item : Item } 86 - ------------------------------------ 87 - -- Position 88 - ------------------------------------ 89 - | Rewind 90 - | Shift 91 - ------------------------------------ 92 - -- Contents 93 - ------------------------------------ 94 - | Clear 95 - | Reset 96 - | Fill Time.Posix (List IdentifiedTrack) 97 - ------------------------------------ 98 - -- Drag & Drop 99 - ------------------------------------ 100 - | DragMsg (DnD.Msg Int) 101 - ------------------------------------ 102 - -- Settings 103 - ------------------------------------ 104 - | ToggleRepeat 105 - | ToggleShuffle 106 - 107 - 108 - update : Msg -> Model -> Return Model Msg Reply 109 - update msg model = 110 - case msg of 111 - Select item -> 112 - return { model | selection = Just item } 113 - 114 - ShowFutureMenu item { index } mouseEvent -> 115 - returnRepliesWithModel 116 - model 117 - [ ShowQueueFutureMenu 118 - (Coordinates.fromTuple mouseEvent.clientPos) 119 - { item = item, itemIndex = index } 120 - ] 121 - 122 - ShowHistoryMenu item mouseEvent -> 123 - returnRepliesWithModel 124 - model 125 - [ ShowQueueHistoryMenu 126 - (Coordinates.fromTuple mouseEvent.clientPos) 127 - { item = item } 128 - ] 129 - 130 - ------------------------------------ 131 - -- Combos 132 - ------------------------------------ 133 - InjectFirstAndPlay identifiedTrack -> 134 - [ identifiedTrack ] 135 - |> InjectFirst { showNotification = False } 136 - |> updateWithModel model 137 - |> andThen (update Shift) 138 - 139 - ------------------------------------ 140 - -- Future 141 - ------------------------------------ 142 - -- # InjectFirst 143 - -- > Add an item in front of the queue. 144 - -- 145 - InjectFirst { showNotification } identifiedTracks -> 146 - let 147 - ( items, tracks ) = 148 - ( List.map (makeItem True) identifiedTracks 149 - , List.map Tuple.second identifiedTracks 150 - ) 151 - 152 - cleanedFuture = 153 - List.foldl 154 - (\track future -> 155 - Fill.cleanAutoGenerated model.shuffle track.id future 156 - ) 157 - model.future 158 - tracks 159 - in 160 - [ case tracks of 161 - [ t ] -> 162 - ("__" ++ t.tags.title ++ "__ will be played next") 163 - |> ShowSuccessNotification 164 - 165 - list -> 166 - list 167 - |> List.length 168 - |> String.fromInt 169 - |> (\s -> "__" ++ s ++ " tracks__ will be played next") 170 - |> ShowSuccessNotification 171 - ] 172 - |> (\list -> ifThenElse showNotification list []) 173 - |> returnRepliesWithModel { model | future = items ++ cleanedFuture } 174 - |> addReply FillQueue 175 - 176 - -- # InjectLast 177 - -- > Add an item after the last manual entry 178 - -- (ie. after the last injected item). 179 - -- 180 - InjectLast { showNotification } identifiedTracks -> 181 - let 182 - ( items, tracks ) = 183 - ( List.map (makeItem True) identifiedTracks 184 - , List.map Tuple.second identifiedTracks 185 - ) 186 - 187 - cleanedFuture = 188 - List.foldl 189 - (\track future -> 190 - Fill.cleanAutoGenerated model.shuffle track.id future 191 - ) 192 - model.future 193 - tracks 194 - 195 - manualItems = 196 - cleanedFuture 197 - |> List.filter (.manualEntry >> (==) True) 198 - |> List.length 199 - 200 - newFuture = 201 - [] 202 - ++ List.take manualItems cleanedFuture 203 - ++ items 204 - ++ List.drop manualItems cleanedFuture 205 - in 206 - [ case tracks of 207 - [ t ] -> 208 - ("__" ++ t.tags.title ++ "__ was added to the queue") 209 - |> ShowSuccessNotification 210 - 211 - list -> 212 - list 213 - |> List.length 214 - |> String.fromInt 215 - |> (\s -> "__" ++ s ++ " tracks__ were added to the queue") 216 - |> ShowSuccessNotification 217 - ] 218 - |> (\list -> ifThenElse showNotification list []) 219 - |> returnRepliesWithModel { model | future = newFuture } 220 - |> addReply FillQueue 221 - 222 - -- # RemoveItem 223 - -- > Remove a future item. 224 - -- 225 - RemoveItem { index, item } -> 226 - let 227 - newFuture = 228 - List.removeAt index model.future 229 - 230 - newIgnored = 231 - if item.manualEntry then 232 - model.ignored 233 - 234 - else 235 - item :: model.ignored 236 - in 237 - returnRepliesWithModel 238 - { model | future = newFuture, ignored = newIgnored } 239 - [ FillQueue ] 240 - 241 - ----------------------------------------- 242 - -- Position 243 - ----------------------------------------- 244 - -- # Rewind 245 - -- > Put the next item in the queue as the current one. 246 - -- 247 - Rewind -> 248 - changeActiveItem 249 - (List.last model.past) 250 - { model 251 - | future = 252 - model.activeItem 253 - |> Maybe.map (\item -> item :: model.future) 254 - |> Maybe.withDefault model.future 255 - , past = 256 - model.past 257 - |> List.init 258 - |> Maybe.withDefault [] 259 - } 260 - 261 - -- # Shift 262 - -- > Put the next item in the queue as the current one. 263 - -- 264 - Shift -> 265 - changeActiveItem 266 - (List.head model.future) 267 - { model 268 - | future = 269 - model.future 270 - |> List.drop 1 271 - , past = 272 - model.activeItem 273 - |> Maybe.map List.singleton 274 - |> Maybe.map (List.append model.past) 275 - |> Maybe.withDefault model.past 276 - } 277 - 278 - ------------------------------------ 279 - -- Contents 280 - ------------------------------------ 281 - -- # Clear 282 - -- 283 - Clear -> 284 - returnRepliesWithModel 285 - { model | future = [], ignored = [] } 286 - [ FillQueue ] 287 - 288 - -- # Fill 289 - -- > Fill the queue with items. 290 - -- 291 - Fill timestamp tracks -> 292 - returnReplyWithModel 293 - (fillQueue timestamp tracks model) 294 - PreloadNextTrack 295 - 296 - -- # Reset 297 - -- > Renew the queue, meaning that the auto-generated items in the queue 298 - -- are removed and new items are added. 299 - -- 300 - Reset -> 301 - let 302 - newFuture = 303 - List.filter (.manualEntry >> (==) True) model.future 304 - in 305 - returnRepliesWithModel 306 - { model | future = newFuture, ignored = [] } 307 - [ FillQueue ] 308 - 309 - ------------------------------------ 310 - -- Drag & Drop 311 - ------------------------------------ 312 - DragMsg dragMsg -> 313 - let 314 - ( dnd, replies ) = 315 - DnD.update dragMsg model.dnd 316 - in 317 - if DnD.hasDropped dnd then 318 - let 319 - ( from, to ) = 320 - ( Maybe.withDefault 0 <| DnD.modelSubject dnd 321 - , Maybe.withDefault 0 <| DnD.modelTarget dnd 322 - ) 323 - 324 - newFuture = 325 - moveItem 326 - { from = from, to = to, shuffle = model.shuffle } 327 - model.future 328 - in 329 - returnRepliesWithModel 330 - { model | dnd = dnd, future = newFuture } 331 - (FillQueue :: replies) 332 - 333 - else 334 - returnRepliesWithModel 335 - { model | dnd = dnd } 336 - replies 337 - 338 - ------------------------------------ 339 - -- Settings 340 - ------------------------------------ 341 - ToggleRepeat -> 342 - ( { model | repeat = not model.repeat } 343 - , Ports.setRepeat (not model.repeat) 344 - , [ SaveEnclosedUserData ] 345 - ) 346 - 347 - ToggleShuffle -> 348 - { model | shuffle = not model.shuffle } 349 - |> update Reset 350 - |> addReply SaveEnclosedUserData 351 - 352 - 353 - updateWithModel : Model -> Msg -> Return Model Msg Reply 354 - updateWithModel model msg = 355 - update msg model 356 - 357 - 358 - 359 - -- 📣 ░░ COMMON 360 - 361 - 362 - changeActiveItem : Maybe Item -> Model -> Return Model Msg Reply 363 - changeActiveItem maybeItem model = 364 - returnRepliesWithModel 365 - { model | activeItem = maybeItem } 366 - [ ActiveQueueItemChanged maybeItem 367 - , FillQueue 368 - ] 369 - 370 - 371 - fillQueue : Time.Posix -> List IdentifiedTrack -> Model -> Model 372 - fillQueue timestamp availableTracks model = 373 - let 374 - nonMissingTracks = 375 - List.filter 376 - (Tuple.second >> .id >> (/=) Tracks.missingId) 377 - availableTracks 378 - in 379 - model 380 - |> (\m -> 381 - -- Empty the ignored list when we are ignoring all the tracks 382 - if List.length model.ignored == List.length nonMissingTracks then 383 - { m | ignored = [] } 384 - 385 - else 386 - m 387 - ) 388 - |> (\m -> 389 - if m.shuffle && List.length model.future >= Fill.queueLength then 390 - m 391 - 392 - else 393 - let 394 - fillState = 395 - { activeItem = m.activeItem 396 - , future = m.future 397 - , ignored = m.ignored 398 - , past = m.past 399 - } 400 - in 401 - -- Fill using the appropiate method 402 - case m.shuffle of 403 - False -> 404 - { m | future = Fill.ordered timestamp nonMissingTracks fillState } 405 - 406 - True -> 407 - { m | future = Fill.shuffled timestamp nonMissingTracks fillState } 408 - ) 409 - 410 - 411 - moveQueueItemToFirst : Model -> { itemIndex : Int } -> Model 412 - moveQueueItemToFirst model { itemIndex } = 413 - model.future 414 - |> moveItem { from = itemIndex, to = 0, shuffle = model.shuffle } 415 - |> (\f -> { model | future = f }) 416 - 417 - 418 - moveQueueItemToLast : Model -> { itemIndex : Int } -> Model 419 - moveQueueItemToLast model { itemIndex } = 420 - let 421 - to = 422 - model.future 423 - |> List.filter (.manualEntry >> (==) True) 424 - |> List.length 425 - in 426 - model.future 427 - |> moveItem { from = itemIndex, to = to, shuffle = model.shuffle } 428 - |> (\f -> { model | future = f }) 429 - 430 - 431 - moveItem : { from : Int, to : Int, shuffle : Bool } -> List Item -> List Item 432 - moveItem { from, to, shuffle } collection = 433 - let 434 - subjectItem = 435 - collection 436 - |> List.getAt from 437 - |> Maybe.map (\s -> { s | manualEntry = True }) 438 - 439 - fixedTarget = 440 - if to > from then 441 - to - 1 442 - 443 - else 444 - to 445 - in 446 - collection 447 - |> List.removeAt from 448 - |> List.indexedFoldr 449 - (\idx existingItem acc -> 450 - if idx == fixedTarget then 451 - case subjectItem of 452 - Just itemToInsert -> 453 - List.append [ itemToInsert, existingItem ] acc 454 - 455 - Nothing -> 456 - existingItem :: acc 457 - 458 - else if idx < fixedTarget then 459 - { existingItem | manualEntry = True } :: acc 460 - 461 - else 462 - existingItem :: acc 463 - ) 464 - [] 465 - |> (if shuffle then 466 - identity 467 - 468 - else 469 - List.filter (.manualEntry >> (==) True) 470 - ) 471 - 472 - 473 - 474 - -- 🗺 475 - 476 - 477 - view : Queue.Page -> Model -> Html Msg 478 - view page model = 479 - UI.Kit.receptacle 480 - { scrolling = not (DnD.isDragging model.dnd) } 481 - (case page of 482 - History -> 483 - historyView model 484 - 485 - Index -> 486 - futureView model 487 - ) 488 - 489 - 490 - 491 - -- 🗺 ░░ FUTURE 492 - 493 - 494 - futureView : Model -> List (Html Msg) 495 - futureView model = 496 - [ ----------------------------------------- 497 - -- Navigation 498 - ----------------------------------------- 499 - UI.Navigation.local 500 - [ ( Icon Icons.arrow_back 501 - , Label Common.backToIndex Hidden 502 - , NavigateToPage Page.Index 503 - ) 504 - , ( Icon Icons.history 505 - , Label "History" Shown 506 - , NavigateToPage (Page.Queue History) 507 - ) 508 - , ( Icon Icons.clear 509 - , Label "Clear" Shown 510 - , PerformMsg Clear 511 - ) 512 - , ( Icon Icons.not_interested 513 - , Label "Reset ignored" Shown 514 - , PerformMsg Reset 515 - ) 516 - ] 517 - 518 - ----------------------------------------- 519 - -- Content 520 - ----------------------------------------- 521 - , if List.isEmpty model.future then 522 - chunk 523 - [ C.relative ] 524 - [ chunk 525 - [ C.absolute, C.left_0, C.top_0 ] 526 - [ UI.Kit.canister [ UI.Kit.h1 "Up next" ] ] 527 - ] 528 - 529 - else 530 - UI.Kit.canister 531 - [ UI.Kit.h1 "Up next" 532 - , model.future 533 - |> List.indexedMap (futureItem model.selection) 534 - |> UI.List.view 535 - (UI.List.Draggable 536 - { model = model.dnd 537 - , toMsg = DragMsg 538 - } 539 - ) 540 - |> chunky [ C.mt_3 ] 541 - ] 542 - 543 - -- 544 - , if List.isEmpty model.future then 545 - UI.Kit.centeredContent 546 - [ slab 547 - Html.a 548 - [ href (Page.toString <| Page.Sources UI.Sources.Page.New) ] 549 - [ C.text_inherit, C.block, C.opacity_30 ] 550 - [ Icons.music_note 64 Inherit ] 551 - , slab 552 - Html.a 553 - [ href (Page.toString <| Page.Sources UI.Sources.Page.New) ] 554 - [ C.text_inherit, C.block, C.leading_normal, C.mt_2, C.opacity_40, C.text_center ] 555 - [ text "Nothing here yet," 556 - , lineBreak 557 - , text "add some music first." 558 - ] 559 - ] 560 - 561 - else 562 - nothing 563 - ] 564 - 565 - 566 - futureItem : Maybe Item -> Int -> Queue.Item -> UI.List.Item Msg 567 - futureItem selection idx item = 568 - let 569 - ( identifiers, track ) = 570 - item.identifiedTrack 571 - 572 - isSelected = 573 - selection 574 - |> Maybe.map (.identifiedTrack >> Tuple.first >> .indexInList) 575 - |> (==) (Just identifiers.indexInList) 576 - 577 - iconFn = 578 - if item.manualEntry then 579 - identity 580 - 581 - else 582 - Icons.wrapped subtleFutureIconClasses 583 - in 584 - { label = 585 - inline 586 - [ C.block 587 - , C.truncate 588 - 589 - -- 590 - , if item.manualEntry || isSelected then 591 - C.text_inherit 592 - 593 - else 594 - C.text_base05 595 - 596 - -- Dark mode 597 - ------------ 598 - , if item.manualEntry || isSelected then 599 - C.dark__text_inherit 600 - 601 - else 602 - C.dark__text_base04 603 - ] 604 - [ inline 605 - [ C.inline_block 606 - , C.mr_2 607 - , C.opacity_60 608 - , C.text_xs 609 - ] 610 - [ text (String.fromInt <| idx + 1), text "." ] 611 - , text (track.tags.artist ++ " - " ++ track.tags.title) 612 - ] 613 - , actions = 614 - [ -- Remove 615 - --------- 616 - { icon = 617 - if item.manualEntry then 618 - iconFn Icons.remove_circle_outline 619 - 620 - else 621 - iconFn Icons.not_interested 622 - , msg = Just (\_ -> RemoveItem { index = idx, item = item }) 623 - , title = ifThenElse item.manualEntry "Remove" "Ignore" 624 - } 625 - 626 - -- Menu 627 - ------- 628 - , { icon = iconFn Icons.more_vert 629 - , msg = Just (ShowFutureMenu item { index = idx }) 630 - , title = "Menu" 631 - } 632 - ] 633 - , msg = Just (Select item) 634 - , isSelected = isSelected 635 - } 636 - 637 - 638 - subtleFutureIconClasses : List String 639 - subtleFutureIconClasses = 640 - [ C.text_gray_500 641 - 642 - -- Dark mode 643 - ------------ 644 - , C.dark__text_base02 645 - ] 646 - 647 - 648 - 649 - -- 🗺 ░░ HISTORY 650 - 651 - 652 - historyView : Model -> List (Html Msg) 653 - historyView model = 654 - [ ----------------------------------------- 655 - -- Navigation 656 - ----------------------------------------- 657 - UI.Navigation.local 658 - [ ( Icon Icons.arrow_back 659 - , Label Common.backToIndex Hidden 660 - , NavigateToPage Page.Index 661 - ) 662 - , ( Icon Icons.update 663 - , Label "Up next" Shown 664 - , NavigateToPage (Page.Queue Index) 665 - ) 666 - ] 667 - 668 - ----------------------------------------- 669 - -- Content 670 - ----------------------------------------- 671 - , if List.isEmpty model.past then 672 - chunk 673 - [ C.relative ] 674 - [ chunk 675 - [ C.absolute, C.left_0, C.top_0 ] 676 - [ UI.Kit.canister [ UI.Kit.h1 "History" ] ] 677 - ] 678 - 679 - else 680 - UI.Kit.canister 681 - [ UI.Kit.h1 "History" 682 - , model.past 683 - |> List.reverse 684 - |> List.indexedMap historyItem 685 - |> UI.List.view UI.List.Normal 686 - |> chunky [ C.mt_3 ] 687 - ] 688 - 689 - -- 690 - , if List.isEmpty model.past then 691 - UI.Kit.centeredContent 692 - [ chunk 693 - [ C.opacity_30 ] 694 - [ Icons.music_note 64 Inherit ] 695 - , chunk 696 - [ C.leading_normal, C.mt_2, C.opacity_40, C.text_center ] 697 - [ text "Nothing here yet," 698 - , lineBreak 699 - , text "play some music first." 700 - ] 701 - ] 702 - 703 - else 704 - nothing 705 - ] 706 - 707 - 708 - historyItem : Int -> Queue.Item -> UI.List.Item Msg 709 - historyItem idx ({ identifiedTrack, manualEntry } as item) = 710 - let 711 - ( _, track ) = 712 - identifiedTrack 713 - in 714 - { label = 715 - inline 716 - [ C.block, C.truncate ] 717 - [ inline 718 - [ C.inline_block, C.text_xs, C.mr_2 ] 719 - [ text (String.fromInt <| idx + 1), text "." ] 720 - , text (track.tags.artist ++ " - " ++ track.tags.title) 721 - ] 722 - , actions = 723 - [ { icon = Icons.more_vert 724 - , msg = Just (ShowHistoryMenu item) 725 - , title = "Menu" 726 - } 727 - ] 728 - , msg = Nothing 729 - , isSelected = False 730 - }
+450 -5
src/Applications/UI/Queue/State.elm
··· 1 1 module UI.Queue.State exposing (..) 2 2 3 + import Coordinates 4 + import Dict 5 + import Html.Events.Extra.Mouse as Mouse 6 + import List.Extra as List 3 7 import Monocle.Lens as Lens exposing (Lens) 8 + import Notifications 9 + import Queue exposing (..) 10 + import Return exposing (andThen, return) 11 + import Return.Ext as Return 12 + import Tracks exposing (..) 13 + import UI.Common.State as Common 14 + import UI.Ports as Ports 15 + import UI.Queue.ContextMenu as Queue 16 + import UI.Queue.Fill as Fill 17 + import UI.Queue.Types as Queue exposing (..) 18 + import UI.Tracks as Tracks 19 + import UI.Types exposing (..) 20 + import UI.User.State.Export as User exposing (..) 4 21 5 22 6 23 7 - -- 🌳 24 + -- 📣 25 + 26 + 27 + update : Queue.Msg -> Manager 28 + update msg = 29 + case msg of 30 + Clear -> 31 + clear 32 + 33 + Reset -> 34 + reset 35 + 36 + Rewind -> 37 + rewind 38 + 39 + Shift -> 40 + shift 41 + 42 + Select a -> 43 + select a 44 + 45 + ShowFutureMenu a b c -> 46 + showFutureMenu a b c 47 + 48 + ShowHistoryMenu a b -> 49 + showHistoryMenu a b 50 + 51 + ToggleRepeat -> 52 + toggleRepeat 53 + 54 + ToggleShuffle -> 55 + toggleShuffle 56 + 57 + ------------------------------------ 58 + -- Future 59 + ------------------------------------ 60 + InjectFirst a b -> 61 + injectFirst a b 62 + 63 + InjectLast a b -> 64 + injectLast a b 65 + 66 + InjectFirstAndPlay a -> 67 + injectFirstAndPlay a 68 + 69 + RemoveItem a -> 70 + removeItem a 71 + 72 + 73 + 74 + -- 🛠 75 + 76 + 77 + changeActiveItem : Maybe Item -> Manager 78 + changeActiveItem maybeItem model = 79 + let 80 + nowPlaying = 81 + Maybe.map .identifiedTrack maybeItem 82 + 83 + portCmd = 84 + maybeItem 85 + |> Maybe.map 86 + (.identifiedTrack >> Tuple.second) 87 + |> Maybe.map 88 + (Queue.makeEngineItem 89 + model.currentTime 90 + model.sources.collection 91 + model.tracks.cached 92 + (if model.rememberProgress then 93 + model.progress 94 + 95 + else 96 + Dict.empty 97 + ) 98 + ) 99 + |> Ports.activeQueueItemChanged 100 + in 101 + { model | nowPlaying = maybeItem } 102 + |> Return.performance 103 + (nowPlaying 104 + |> Tracks.SetNowPlaying 105 + |> TracksMsg 106 + ) 107 + |> andThen fill 108 + |> Return.command portCmd 109 + 110 + 111 + clear : Manager 112 + clear model = 113 + fill { model | playingNext = [], dontPlay = [] } 114 + 115 + 116 + fill : Manager 117 + fill model = 118 + let 119 + ( availableTracks, timestamp ) = 120 + ( model.tracks.collection.harvested 121 + , model.currentTime 122 + ) 123 + 124 + nonMissingTracks = 125 + List.filter 126 + (Tuple.second >> .id >> (/=) Tracks.missingId) 127 + availableTracks 128 + in 129 + model 130 + |> (\m -> 131 + -- Empty the ignored list when we are ignoring all the tracks 132 + if List.length model.dontPlay == List.length nonMissingTracks then 133 + { m | dontPlay = [] } 134 + 135 + else 136 + m 137 + ) 138 + |> (\m -> 139 + if m.shuffle && List.length model.playingNext >= Fill.queueLength then 140 + m 141 + 142 + else 143 + let 144 + fillState = 145 + { activeItem = m.nowPlaying 146 + , future = m.playingNext 147 + , ignored = m.dontPlay 148 + , past = m.playedPreviously 149 + } 150 + in 151 + -- Fill using the appropiate method 152 + case m.shuffle of 153 + False -> 154 + { m | playingNext = Fill.ordered timestamp nonMissingTracks fillState } 155 + 156 + True -> 157 + { m | playingNext = Fill.shuffled timestamp nonMissingTracks fillState } 158 + ) 159 + |> preloadNext 160 + 161 + 162 + preloadNext : Manager 163 + preloadNext model = 164 + case List.head model.playingNext of 165 + Just item -> 166 + item 167 + |> .identifiedTrack 168 + |> Tuple.second 169 + |> Queue.makeEngineItem 170 + model.currentTime 171 + model.sources.collection 172 + model.tracks.cached 173 + (if model.rememberProgress then 174 + model.progress 175 + 176 + else 177 + Dict.empty 178 + ) 179 + |> Ports.preloadAudio 180 + |> return model 181 + 182 + Nothing -> 183 + Return.singleton model 184 + 185 + 186 + rewind : Manager 187 + rewind model = 188 + changeActiveItem 189 + (List.last model.playedPreviously) 190 + { model 191 + | playingNext = 192 + model.nowPlaying 193 + |> Maybe.map (\item -> item :: model.playingNext) 194 + |> Maybe.withDefault model.playingNext 195 + , playedPreviously = 196 + model.playedPreviously 197 + |> List.init 198 + |> Maybe.withDefault [] 199 + } 200 + 201 + 202 + {-| Renew the queue, meaning that the auto-generated items in the queue are removed and new items are added. 203 + -} 204 + reset : Manager 205 + reset model = 206 + model.playingNext 207 + |> List.filter (.manualEntry >> (==) True) 208 + |> (\f -> { model | playingNext = f, dontPlay = [] }) 209 + |> fill 210 + 211 + 212 + select : Item -> Manager 213 + select item model = 214 + Return.singleton { model | selectedQueueItem = Just item } 215 + 216 + 217 + shift : Manager 218 + shift model = 219 + changeActiveItem 220 + (List.head model.playingNext) 221 + { model 222 + | playingNext = 223 + model.playingNext 224 + |> List.drop 1 225 + , playedPreviously = 226 + model.nowPlaying 227 + |> Maybe.map List.singleton 228 + |> Maybe.map (List.append model.playedPreviously) 229 + |> Maybe.withDefault model.playedPreviously 230 + } 231 + 232 + 233 + showFutureMenu : Item -> { index : Int } -> Mouse.Event -> Manager 234 + showFutureMenu item { index } mouseEvent model = 235 + mouseEvent.clientPos 236 + |> Coordinates.fromTuple 237 + |> Queue.futureMenu 238 + { cached = model.tracks.cached 239 + , cachingInProgress = model.tracks.cachingInProgress 240 + , itemIndex = index 241 + } 242 + item 243 + |> Just 244 + |> (\c -> { model | contextMenu = c }) 245 + |> Return.singleton 246 + 247 + 248 + showHistoryMenu : Item -> Mouse.Event -> Manager 249 + showHistoryMenu item mouseEvent model = 250 + mouseEvent.clientPos 251 + |> Coordinates.fromTuple 252 + |> Queue.historyMenu 253 + { cached = model.tracks.cached 254 + , cachingInProgress = model.tracks.cachingInProgress 255 + } 256 + item 257 + |> Just 258 + |> (\c -> { model | contextMenu = c }) 259 + |> Return.singleton 260 + 261 + 262 + toggleRepeat : Manager 263 + toggleRepeat model = 264 + { model | repeat = not model.repeat } 265 + |> saveEnclosedUserData 266 + |> Return.effect_ (.repeat >> Ports.setRepeat) 267 + 268 + 269 + toggleShuffle : Manager 270 + toggleShuffle model = 271 + { model | shuffle = not model.shuffle } 272 + |> reset 273 + |> andThen saveEnclosedUserData 274 + 275 + 276 + 277 + -- 🛠 ░░ FUTURE 278 + 279 + 280 + {-| Add an item in front of the queue. 281 + -} 282 + injectFirst : { showNotification : Bool } -> List IdentifiedTrack -> Manager 283 + injectFirst { showNotification } identifiedTracks model = 284 + let 285 + ( items, tracks ) = 286 + ( List.map (makeItem True) identifiedTracks 287 + , List.map Tuple.second identifiedTracks 288 + ) 289 + 290 + cleanedFuture = 291 + List.foldl 292 + (.id >> Fill.cleanAutoGenerated model.shuffle) 293 + model.playingNext 294 + tracks 295 + 296 + notification = 297 + case tracks of 298 + [ t ] -> 299 + ("__" ++ t.tags.title ++ "__ will be played next") 300 + |> Notifications.success 301 + 302 + list -> 303 + list 304 + |> List.length 305 + |> String.fromInt 306 + |> (\s -> "__" ++ s ++ " tracks__ will be played next") 307 + |> Notifications.success 308 + in 309 + { model | playingNext = items ++ cleanedFuture } 310 + |> (if showNotification then 311 + Common.showNotification notification 312 + 313 + else 314 + Return.singleton 315 + ) 316 + |> andThen fill 317 + 318 + 319 + injectFirstAndPlay : IdentifiedTrack -> Manager 320 + injectFirstAndPlay identifiedTrack model = 321 + model 322 + |> injectFirst { showNotification = False } [ identifiedTrack ] 323 + |> andThen shift 324 + 325 + 326 + {-| Add an item after the last manual entry 327 + (ie. after the last injected item). 328 + -} 329 + injectLast : { showNotification : Bool } -> List IdentifiedTrack -> Manager 330 + injectLast { showNotification } identifiedTracks model = 331 + let 332 + ( items, tracks ) = 333 + ( List.map (makeItem True) identifiedTracks 334 + , List.map Tuple.second identifiedTracks 335 + ) 336 + 337 + cleanedFuture = 338 + List.foldl 339 + (.id >> Fill.cleanAutoGenerated model.shuffle) 340 + model.playingNext 341 + tracks 342 + 343 + manualItems = 344 + cleanedFuture 345 + |> List.filter (.manualEntry >> (==) True) 346 + |> List.length 347 + 348 + newFuture = 349 + [] 350 + ++ List.take manualItems cleanedFuture 351 + ++ items 352 + ++ List.drop manualItems cleanedFuture 353 + 354 + notification = 355 + case tracks of 356 + [ t ] -> 357 + ("__" ++ t.tags.title ++ "__ was added to the queue") 358 + |> Notifications.success 359 + 360 + list -> 361 + list 362 + |> List.length 363 + |> String.fromInt 364 + |> (\s -> "__" ++ s ++ " tracks__ were added to the queue") 365 + |> Notifications.success 366 + in 367 + { model | playingNext = newFuture } 368 + |> (if showNotification then 369 + Common.showNotification notification 370 + 371 + else 372 + Return.singleton 373 + ) 374 + |> andThen fill 375 + 376 + 377 + moveQueueItemToFirst : { itemIndex : Int } -> Manager 378 + moveQueueItemToFirst { itemIndex } model = 379 + model.playingNext 380 + |> moveItem { from = itemIndex, to = 0, shuffle = model.shuffle } 381 + |> (\f -> { model | playingNext = f }) 382 + |> fill 383 + 384 + 385 + moveQueueItemToLast : { itemIndex : Int } -> Manager 386 + moveQueueItemToLast { itemIndex } model = 387 + let 388 + to = 389 + model.playingNext 390 + |> List.filter (.manualEntry >> (==) True) 391 + |> List.length 392 + in 393 + model.playingNext 394 + |> moveItem { from = itemIndex, to = to, shuffle = model.shuffle } 395 + |> (\f -> { model | playingNext = f }) 396 + |> fill 397 + 398 + 399 + removeItem : { index : Int, item : Item } -> Manager 400 + removeItem { index, item } model = 401 + let 402 + newFuture = 403 + List.removeAt index model.playingNext 404 + 405 + newIgnored = 406 + if item.manualEntry then 407 + model.dontPlay 8 408 409 + else 410 + item :: model.dontPlay 411 + in 412 + fill { model | playingNext = newFuture, dontPlay = newIgnored } 9 413 10 - lens = 11 - { get = .queue 12 - , set = \queue ui -> { ui | queue = queue } 13 - } 414 + 415 + 416 + -- ⚗️ 417 + 418 + 419 + moveItem : { from : Int, to : Int, shuffle : Bool } -> List Item -> List Item 420 + moveItem { from, to, shuffle } collection = 421 + let 422 + subjectItem = 423 + collection 424 + |> List.getAt from 425 + |> Maybe.map (\s -> { s | manualEntry = True }) 426 + 427 + fixedTarget = 428 + if to > from then 429 + to - 1 430 + 431 + else 432 + to 433 + in 434 + collection 435 + |> List.removeAt from 436 + |> List.indexedFoldr 437 + (\idx existingItem acc -> 438 + if idx == fixedTarget then 439 + case subjectItem of 440 + Just itemToInsert -> 441 + List.append [ itemToInsert, existingItem ] acc 442 + 443 + Nothing -> 444 + existingItem :: acc 445 + 446 + else if idx < fixedTarget then 447 + { existingItem | manualEntry = True } :: acc 448 + 449 + else 450 + existingItem :: acc 451 + ) 452 + [] 453 + |> (if shuffle then 454 + identity 455 + 456 + else 457 + List.filter (.manualEntry >> (==) True) 458 + )
+30
src/Applications/UI/Queue/Types.elm
··· 1 + module UI.Queue.Types exposing (..) 2 + 3 + import Html.Events.Extra.Mouse as Mouse 4 + import Queue exposing (Item) 5 + import Time 6 + import Tracks exposing (IdentifiedTrack) 7 + import UI.DnD as DnD 8 + 9 + 10 + 11 + -- 📣 12 + 13 + 14 + type Msg 15 + = Clear 16 + | Reset 17 + | Rewind 18 + | Select Item 19 + | Shift 20 + | ShowFutureMenu Item { index : Int } Mouse.Event 21 + | ShowHistoryMenu Item Mouse.Event 22 + | ToggleRepeat 23 + | ToggleShuffle 24 + ------------------------------------ 25 + -- Future 26 + ------------------------------------ 27 + | InjectFirst { showNotification : Bool } (List IdentifiedTrack) 28 + | InjectLast { showNotification : Bool } (List IdentifiedTrack) 29 + | InjectFirstAndPlay IdentifiedTrack 30 + | RemoveItem { index : Int, item : Item }
+307
src/Applications/UI/Queue/View.elm
··· 1 + module UI.Queue.View exposing (view) 2 + 3 + import Chunky exposing (..) 4 + import Common 5 + import Conditional exposing (..) 6 + import Coordinates 7 + import Css.Classes as C 8 + import Html exposing (Html, text) 9 + import Html.Attributes exposing (href) 10 + import Html.Events.Extra.Mouse as Mouse 11 + import Html.Lazy as Lazy 12 + import Icons 13 + import List.Extra as List 14 + import Material.Icons as Icons 15 + import Material.Icons.Types exposing (Coloring(..)) 16 + import Queue exposing (..) 17 + import Time 18 + import Tracks exposing (IdentifiedTrack) 19 + import UI.DnD as DnD 20 + import UI.Kit 21 + import UI.List 22 + import UI.Navigation exposing (..) 23 + import UI.Page as Page 24 + import UI.Ports as Ports 25 + import UI.Queue.Fill as Fill 26 + import UI.Queue.Page as Queue exposing (Page(..)) 27 + import UI.Queue.Types as Queue exposing (..) 28 + import UI.Reply exposing (Reply(..)) 29 + import UI.Sources.Page 30 + import UI.Types as UI exposing (..) 31 + 32 + 33 + 34 + -- 🗺 35 + 36 + 37 + view : Queue.Page -> UI.Model -> Html UI.Msg 38 + view page model = 39 + case page of 40 + History -> 41 + Lazy.lazy2 42 + historyView 43 + model.playedPreviously 44 + model.dnd 45 + 46 + Index -> 47 + Lazy.lazy3 48 + futureView 49 + model.playingNext 50 + model.selectedQueueItem 51 + model.dnd 52 + 53 + 54 + 55 + -- 🗺 ░░ FUTURE 56 + 57 + 58 + futureView : List Queue.Item -> Maybe Queue.Item -> DnD.Model Int -> Html UI.Msg 59 + futureView playingNext selectedQueueItem dnd = 60 + UI.Kit.receptacle 61 + { scrolling = not (DnD.isDragging dnd) } 62 + [ ----------------------------------------- 63 + -- Navigation 64 + ----------------------------------------- 65 + UI.Navigation.local 66 + [ ( Icon Icons.arrow_back 67 + , Label Common.backToIndex Hidden 68 + , NavigateToPage Page.Index 69 + ) 70 + , ( Icon Icons.history 71 + , Label "History" Shown 72 + , NavigateToPage (Page.Queue History) 73 + ) 74 + , ( Icon Icons.clear 75 + , Label "Clear" Shown 76 + , PerformMsg (QueueMsg Clear) 77 + ) 78 + , ( Icon Icons.not_interested 79 + , Label "Reset ignored" Shown 80 + , PerformMsg (QueueMsg Reset) 81 + ) 82 + ] 83 + 84 + ----------------------------------------- 85 + -- Content 86 + ----------------------------------------- 87 + , if List.isEmpty playingNext then 88 + chunk 89 + [ C.relative ] 90 + [ chunk 91 + [ C.absolute, C.left_0, C.top_0 ] 92 + [ UI.Kit.canister [ UI.Kit.h1 "Up next" ] ] 93 + ] 94 + 95 + else 96 + UI.Kit.canister 97 + [ UI.Kit.h1 "Up next" 98 + , playingNext 99 + |> List.indexedMap (futureItem selectedQueueItem) 100 + |> UI.List.view 101 + (UI.List.Draggable 102 + { model = dnd 103 + , toMsg = UI.DnD 104 + } 105 + ) 106 + |> chunky [ C.mt_3 ] 107 + ] 108 + 109 + -- 110 + , if List.isEmpty playingNext then 111 + UI.Kit.centeredContent 112 + [ slab 113 + Html.a 114 + [ href (Page.toString <| Page.Sources UI.Sources.Page.New) ] 115 + [ C.text_inherit, C.block, C.opacity_30 ] 116 + [ Icons.music_note 64 Inherit ] 117 + , slab 118 + Html.a 119 + [ href (Page.toString <| Page.Sources UI.Sources.Page.New) ] 120 + [ C.text_inherit, C.block, C.leading_normal, C.mt_2, C.opacity_40, C.text_center ] 121 + [ text "Nothing here yet," 122 + , lineBreak 123 + , text "add some music first." 124 + ] 125 + ] 126 + 127 + else 128 + nothing 129 + ] 130 + 131 + 132 + futureItem : Maybe Item -> Int -> Queue.Item -> UI.List.Item UI.Msg 133 + futureItem selectedQueueItem idx item = 134 + let 135 + ( identifiers, track ) = 136 + item.identifiedTrack 137 + 138 + isSelected = 139 + selectedQueueItem 140 + |> Maybe.map (.identifiedTrack >> Tuple.first >> .indexInList) 141 + |> (==) (Just identifiers.indexInList) 142 + 143 + iconFn = 144 + if item.manualEntry then 145 + identity 146 + 147 + else 148 + Icons.wrapped subtleFutureIconClasses 149 + in 150 + { label = 151 + inline 152 + [ C.block 153 + , C.truncate 154 + 155 + -- 156 + , if item.manualEntry || isSelected then 157 + C.text_inherit 158 + 159 + else 160 + C.text_base05 161 + 162 + -- Dark mode 163 + ------------ 164 + , if item.manualEntry || isSelected then 165 + C.dark__text_inherit 166 + 167 + else 168 + C.dark__text_base04 169 + ] 170 + [ inline 171 + [ C.inline_block 172 + , C.mr_2 173 + , C.opacity_60 174 + , C.text_xs 175 + ] 176 + [ text (String.fromInt <| idx + 1), text "." ] 177 + , text (track.tags.artist ++ " - " ++ track.tags.title) 178 + ] 179 + , actions = 180 + [ -- Remove 181 + --------- 182 + { icon = 183 + if item.manualEntry then 184 + iconFn Icons.remove_circle_outline 185 + 186 + else 187 + iconFn Icons.not_interested 188 + , msg = 189 + { index = idx, item = item } 190 + |> RemoveItem 191 + |> QueueMsg 192 + |> always 193 + |> Just 194 + , title = 195 + ifThenElse item.manualEntry "Remove" "Ignore" 196 + } 197 + 198 + -- Menu 199 + ------- 200 + , { icon = 201 + iconFn Icons.more_vert 202 + , msg = 203 + Just (QueueMsg << ShowFutureMenu item { index = idx }) 204 + , title = 205 + "Menu" 206 + } 207 + ] 208 + , msg = Just (QueueMsg <| Select item) 209 + , isSelected = isSelected 210 + } 211 + 212 + 213 + subtleFutureIconClasses : List String 214 + subtleFutureIconClasses = 215 + [ C.text_gray_500 216 + 217 + -- Dark mode 218 + ------------ 219 + , C.dark__text_base02 220 + ] 221 + 222 + 223 + 224 + -- 🗺 ░░ HISTORY 225 + 226 + 227 + historyView : List Queue.Item -> DnD.Model Int -> Html UI.Msg 228 + historyView playedPreviously dnd = 229 + UI.Kit.receptacle 230 + { scrolling = not (DnD.isDragging dnd) } 231 + [ ----------------------------------------- 232 + -- Navigation 233 + ----------------------------------------- 234 + UI.Navigation.local 235 + [ ( Icon Icons.arrow_back 236 + , Label Common.backToIndex Hidden 237 + , NavigateToPage Page.Index 238 + ) 239 + , ( Icon Icons.update 240 + , Label "Up next" Shown 241 + , NavigateToPage (Page.Queue Index) 242 + ) 243 + ] 244 + 245 + ----------------------------------------- 246 + -- Content 247 + ----------------------------------------- 248 + , if List.isEmpty playedPreviously then 249 + chunk 250 + [ C.relative ] 251 + [ chunk 252 + [ C.absolute, C.left_0, C.top_0 ] 253 + [ UI.Kit.canister [ UI.Kit.h1 "History" ] ] 254 + ] 255 + 256 + else 257 + UI.Kit.canister 258 + [ UI.Kit.h1 "History" 259 + , playedPreviously 260 + |> List.reverse 261 + |> List.indexedMap historyItem 262 + |> UI.List.view UI.List.Normal 263 + |> chunky [ C.mt_3 ] 264 + ] 265 + 266 + -- 267 + , if List.isEmpty playedPreviously then 268 + UI.Kit.centeredContent 269 + [ chunk 270 + [ C.opacity_30 ] 271 + [ Icons.music_note 64 Inherit ] 272 + , chunk 273 + [ C.leading_normal, C.mt_2, C.opacity_40, C.text_center ] 274 + [ text "Nothing here yet," 275 + , lineBreak 276 + , text "play some music first." 277 + ] 278 + ] 279 + 280 + else 281 + nothing 282 + ] 283 + 284 + 285 + historyItem : Int -> Queue.Item -> UI.List.Item UI.Msg 286 + historyItem idx ({ identifiedTrack, manualEntry } as item) = 287 + let 288 + ( _, track ) = 289 + identifiedTrack 290 + in 291 + { label = 292 + inline 293 + [ C.block, C.truncate ] 294 + [ inline 295 + [ C.inline_block, C.text_xs, C.mr_2 ] 296 + [ text (String.fromInt <| idx + 1), text "." ] 297 + , text (track.tags.artist ++ " - " ++ track.tags.title) 298 + ] 299 + , actions = 300 + [ { icon = Icons.more_vert 301 + , msg = Just (QueueMsg << ShowHistoryMenu item) 302 + , title = "Menu" 303 + } 304 + ] 305 + , msg = Nothing 306 + , isSelected = False 307 + }
-6
src/Applications/UI/Reply.elm
··· 19 19 -- 20 20 | CopyToClipboard String 21 21 | GoToPage Page 22 - | StartedDragging 23 22 | ToggleLoadingScreen Switch 24 23 ----------------------------------------- 25 24 -- Audio ··· 42 41 | ReplyViaContextMenu Reply 43 42 | ShowMoreAuthenticationOptions Coordinates 44 43 | ShowPlaylistListMenu Coordinates Playlist 45 - | ShowQueueFutureMenu Coordinates { item : Queue.Item, itemIndex : Int } 46 - | ShowQueueHistoryMenu Coordinates { item : Queue.Item } 47 44 | ShowSourceContextMenu Coordinates Source 48 45 | ShowTracksContextMenu Coordinates { alt : Bool } (List IdentifiedTrack) 49 46 | ShowTracksViewMenu Coordinates (Maybe Tracks.Grouping) ··· 78 75 ----------------------------------------- 79 76 -- Queue 80 77 ----------------------------------------- 81 - | ActiveQueueItemChanged (Maybe Queue.Item) 82 78 | AddToQueue { inFront : Bool, tracks : List IdentifiedTrack } 83 - | FillQueue 84 79 | MoveQueueItemToFirst { itemIndex : Int } 85 80 | MoveQueueItemToLast { itemIndex : Int } 86 81 | PlayTrack IdentifiedTrack ··· 99 94 | ExternalSourceAuthorization (String -> String) 100 95 | ForceTracksRerender 101 96 | GroupTracksBy Tracks.Grouping 102 - | PreloadNextTrack 103 97 | ProcessSources (List Source) 104 98 | RemoveSourceFromCollection { sourceId : String } 105 99 | RemoveTracksFromCache (List Track)
+15 -73
src/Applications/UI/Reply/Translate.elm
··· 41 41 import UI.Playlists.ContextMenu as Playlists 42 42 import UI.Playlists.Directory 43 43 import UI.Ports as Ports 44 - import UI.Queue as Queue 45 44 import UI.Queue.ContextMenu as Queue 45 + import UI.Queue.State as Queue 46 + import UI.Queue.Types as Queue 46 47 import UI.Reply as Reply exposing (Reply(..)) 47 48 import UI.Routing.State as Routing 48 49 import UI.Settings as Settings ··· 77 78 78 79 GoToPage page -> 79 80 Routing.changeUrlUsingPage page model 80 - 81 - StartedDragging -> 82 - Return.singleton { model | isDragging = True } 83 81 84 82 Reply.ToggleLoadingScreen a -> 85 83 Interface.toggleLoadingScreen a model ··· 153 151 , playlistToActivate = Nothing 154 152 155 153 -- 156 - , queue = 157 - Queue.initialModel 154 + , dontPlay = [] 155 + , nowPlaying = Nothing 156 + , playedPreviously = [] 157 + , playingNext = [] 158 + , selectedQueueItem = Nothing 159 + 160 + -- 161 + , repeat = False 162 + , shuffle = False 163 + 164 + -- 158 165 , sources = 159 166 { sources 160 167 | collection = [] ··· 198 205 Reply.ShowPlaylistListMenu coordinates playlist -> 199 206 Return.singleton { model | contextMenu = Just (Playlists.listMenu playlist model.tracks.collection.identified model.confirmation coordinates) } 200 207 201 - ShowQueueFutureMenu coordinates { item, itemIndex } -> 202 - Return.singleton { model | contextMenu = Just (Queue.futureMenu { cached = model.tracks.cached, cachingInProgress = model.tracks.cachingInProgress, itemIndex = itemIndex } item coordinates) } 203 - 204 - ShowQueueHistoryMenu coordinates { item } -> 205 - Return.singleton { model | contextMenu = Just (Queue.historyMenu { cached = model.tracks.cached, cachingInProgress = model.tracks.cachingInProgress } item coordinates) } 206 - 207 208 ShowSourceContextMenu coordinates source -> 208 209 Return.singleton { model | contextMenu = Just (Sources.sourceMenu source coordinates) } 209 210 ··· 360 361 ----------------------------------------- 361 362 -- Queue 362 363 ----------------------------------------- 363 - ActiveQueueItemChanged maybeQueueItem -> 364 - let 365 - nowPlaying = 366 - Maybe.map .identifiedTrack maybeQueueItem 367 - 368 - portCmd = 369 - maybeQueueItem 370 - |> Maybe.map 371 - (.identifiedTrack >> Tuple.second) 372 - |> Maybe.map 373 - (Queue.makeEngineItem 374 - model.currentTime 375 - model.sources.collection 376 - model.tracks.cached 377 - (if model.rememberProgress then 378 - model.progress 379 - 380 - else 381 - Dict.empty 382 - ) 383 - ) 384 - |> Ports.activeQueueItemChanged 385 - in 386 - model 387 - |> Return.performance (TracksMsg <| Tracks.SetNowPlaying nowPlaying) 388 - |> Return.command portCmd 389 - 390 364 AddToQueue { inFront, tracks } -> 391 365 (if inFront then 392 366 Queue.InjectFirst ··· 398 372 |> QueueMsg 399 373 |> Return.performanceF model 400 374 401 - FillQueue -> 402 - model.tracks.collection.harvested 403 - |> Queue.Fill model.currentTime 404 - |> QueueMsg 405 - |> Return.performanceF model 406 - 407 375 MoveQueueItemToFirst args -> 408 - translate 409 - FillQueue 410 - { model | queue = Queue.moveQueueItemToFirst model.queue args } 376 + Queue.moveQueueItemToFirst args model 411 377 412 378 MoveQueueItemToLast args -> 413 - translate 414 - FillQueue 415 - { model | queue = Queue.moveQueueItemToLast model.queue args } 379 + Queue.moveQueueItemToLast args model 416 380 417 381 PlayTrack identifiedTrack -> 418 382 identifiedTrack ··· 499 463 |> Tracks.GroupBy 500 464 |> TracksMsg 501 465 |> Return.performanceF model 502 - 503 - PreloadNextTrack -> 504 - case List.head model.queue.future of 505 - Just item -> 506 - item 507 - |> .identifiedTrack 508 - |> Tuple.second 509 - |> Queue.makeEngineItem 510 - model.currentTime 511 - model.sources.collection 512 - model.tracks.cached 513 - (if model.rememberProgress then 514 - model.progress 515 - 516 - else 517 - Dict.empty 518 - ) 519 - |> Ports.preloadAudio 520 - |> return model 521 - 522 - Nothing -> 523 - Return.singleton model 524 466 525 467 ProcessSources [] -> 526 468 Return.singleton model
-1
src/Applications/UI/Tracks/ContextMenu.elm
··· 10 10 import Sources exposing (Source) 11 11 import Time 12 12 import Tracks exposing (Grouping(..), IdentifiedTrack) 13 - import UI.Queue as Queue 14 13 import UI.Reply exposing (Reply(..)) 15 14 import UI.Tracks as Tracks 16 15
+3 -3
src/Applications/UI/Tracks/Scene/List.elm
··· 73 73 74 74 DragAndDropMsg subMsg -> 75 75 let 76 - ( newDnD, uiReplies ) = 76 + -- TODO: Set { model | dragging = True } using `initiated` 77 + ( newDnD, { initiated } ) = 77 78 DnD.update subMsg model.dnd 78 79 in 79 80 if DnD.hasDropped newDnD then ··· 82 83 [ MoveTrackInSelectedPlaylist 83 84 { to = Maybe.withDefault 0 (DnD.modelTarget newDnD) 84 85 } 85 - , Transcend uiReplies 86 86 ] 87 87 88 88 else 89 89 returnRepliesWithModel 90 90 { model | dnd = newDnD } 91 - [ Transcend uiReplies ] 91 + [] 92 92 93 93 94 94
+19 -5
src/Applications/UI/Types.elm
··· 30 30 import Tracks 31 31 import Tracks.Encoding as Tracks 32 32 import UI.Authentication.Types as Authentication 33 + import UI.DnD as DnD 33 34 import UI.Notifications 34 35 import UI.Page as Page exposing (Page) 35 - import UI.Queue as Queue 36 - import UI.Queue.ContextMenu as Queue 36 + import UI.Queue.Types as Queue 37 37 import UI.Reply as Reply exposing (Reply(..)) 38 38 import UI.Sources as Sources 39 39 import UI.Sources.ContextMenu as Sources ··· 66 66 , currentTime : Time.Posix 67 67 , darkMode : Bool 68 68 , downloading : Maybe { notificationId : Int } 69 + , dnd : DnD.Model Int 69 70 , focusedOnInput : Bool 70 71 , isDragging : Bool 71 72 , isLoading : Bool ··· 127 128 , playlistToActivate : Maybe String 128 129 129 130 ----------------------------------------- 131 + -- Queue 132 + ----------------------------------------- 133 + , dontPlay : List Queue.Item 134 + , nowPlaying : Maybe Queue.Item 135 + , playedPreviously : List Queue.Item 136 + , playingNext : List Queue.Item 137 + , selectedQueueItem : Maybe Queue.Item 138 + 139 + -- 140 + , repeat : Bool 141 + , shuffle : Bool 142 + 143 + ----------------------------------------- 130 144 -- 🦉 Nested 131 145 ----------------------------------------- 132 146 , authentication : Authentication.State ··· 134 148 ----------------------------------------- 135 149 -- Children (TODO) 136 150 ----------------------------------------- 137 - , queue : Queue.Model 138 151 , sources : Sources.Model 139 152 , tracks : Tracks.Model 140 153 } ··· 165 178 | Stop 166 179 | TogglePlay 167 180 ----------------------------------------- 168 - -- Authentication 181 + -- Authentication (TODO: Move to Auth.Types) 169 182 ----------------------------------------- 170 183 | AuthenticationBootFailure String 171 184 | MissingSecretKey Json.Decode.Value ··· 189 202 ----------------------------------------- 190 203 | Blur 191 204 | Debounce (Debouncer.Msg Msg) 205 + | DnD (DnD.Msg Int) 192 206 | FocusedOnInput 193 207 | HideOverlay 194 208 | PreferredColorSchemaChanged { dark : Bool } ··· 250 264 -- 🦉 Nested 251 265 ----------------------------------------- 252 266 | AuthenticationMsg Authentication.Msg 267 + | QueueMsg Queue.Msg 253 268 ----------------------------------------- 254 269 -- Children (TODO) 255 270 ----------------------------------------- 256 - | QueueMsg Queue.Msg 257 271 | SourcesMsg Sources.Msg 258 272 | TracksMsg Tracks.Msg 259 273
+2 -2
src/Applications/UI/User/State/Export.elm
··· 18 18 , grouping = model.tracks.grouping 19 19 , onlyShowCachedTracks = model.tracks.cachedOnly 20 20 , onlyShowFavourites = model.tracks.favouritesOnly 21 - , repeat = model.queue.repeat 21 + , repeat = model.repeat 22 22 , searchTerm = model.tracks.searchTerm 23 23 , selectedPlaylist = Maybe.map .name model.tracks.selectedPlaylist 24 - , shuffle = model.queue.shuffle 24 + , shuffle = model.shuffle 25 25 , sortBy = model.tracks.sortBy 26 26 , sortDirection = model.tracks.sortDirection 27 27 }
+5 -12
src/Applications/UI/User/State/Import.elm
··· 187 187 importEnclosed : Json.Decode.Value -> Model -> Return3.Return Model Msg Reply 188 188 importEnclosed value model = 189 189 let 190 - { queue, tracks } = 190 + { tracks } = 191 191 model 192 192 193 193 equalizerSettings = ··· 204 204 , volume = data.equalizerSettings.volume 205 205 } 206 206 207 - newQueue = 208 - { queue 209 - | repeat = data.repeat 210 - , shuffle = data.shuffle 211 - } 212 - 213 207 newTracks = 214 208 { tracks 215 209 | cached = data.cachedTracks ··· 222 216 } 223 217 in 224 218 ( { model 225 - | queue = newQueue 226 - , tracks = newTracks 227 - 228 - -- 229 - , eqSettings = newEqualizerSettings 219 + | eqSettings = newEqualizerSettings 230 220 , playlistToActivate = data.selectedPlaylist 221 + , repeat = data.repeat 222 + , shuffle = data.shuffle 223 + , tracks = newTracks 231 224 } 232 225 -- 233 226 , Cmd.batch
+6 -9
src/Applications/UI/View.elm
··· 37 37 import UI.Page as Page 38 38 import UI.Playlists.ContextMenu as Playlists 39 39 import UI.Playlists.View as Playlists 40 - import UI.Queue as Queue 41 - import UI.Queue.ContextMenu as Queue 40 + import UI.Queue.View as Queue 42 41 import UI.Reply exposing (Reply(..)) 43 42 import UI.Settings as Settings 44 43 import UI.Settings.Page ··· 83 82 , on "touchend" (Json.Decode.succeed StoppedDragging) 84 83 ] 85 84 86 - else if Maybe.isJust model.queue.selection then 85 + else if Maybe.isJust model.selectedQueueItem then 87 86 [ on "tap" (Json.Decode.succeed RemoveQueueSelection) ] 88 87 89 88 else if not (List.isEmpty model.tracks.selectedTrackIndexes) then ··· 189 188 model.extractedBackdropColor 190 189 191 190 Page.Queue subPage -> 192 - model.queue 193 - |> Lazy.lazy2 Queue.view subPage 194 - |> Html.map QueueMsg 191 + Queue.view subPage model 195 192 196 193 Page.Settings subPage -> 197 194 { authenticationMethod = Authentication.extractMethod model.authentication ··· 219 216 ----------------------------------------- 220 217 , Html.map Reply 221 218 (UI.Console.view 222 - model.queue.activeItem 223 - model.queue.repeat 224 - model.queue.shuffle 219 + model.nowPlaying 220 + model.repeat 221 + model.shuffle 225 222 { stalled = model.audioHasStalled 226 223 , loading = model.audioIsLoading 227 224 , playing = model.audioIsPlaying