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.

Add Queue and connect it with Tracks

+801 -37
+1 -1
elm.json
··· 20 20 "elm/html": "1.0.0", 21 21 "elm/http": "2.0.0", 22 22 "elm/json": "1.1.2", 23 + "elm/random": "1.0.0", 23 24 "elm/regex": "1.0.0", 24 25 "elm/svg": "1.0.1", 25 26 "elm/time": "1.0.0", ··· 42 43 "indirect": { 43 44 "Skinney/murmur3": "2.0.8", 44 45 "elm/parser": "1.1.0", 45 - "elm/random": "1.0.0", 46 46 "elm-explorations/test": "1.2.0", 47 47 "fredcy/elm-parseint": "2.0.1", 48 48 "jinjor/elm-xml-parser": "2.0.0",
+70 -5
src/Applications/UI.elm
··· 33 33 import UI.Navigation as Navigation 34 34 import UI.Page as Page 35 35 import UI.Ports as Ports 36 + import UI.Queue as Queue 37 + import UI.Queue.Common 38 + import UI.Queue.Core as Queue 36 39 import UI.Reply as Reply exposing (Reply(..)) 37 40 import UI.Settings as Settings 38 41 import UI.Settings.Page ··· 70 73 ( ----------------------------------------- 71 74 -- Initial model 72 75 ----------------------------------------- 73 - { isAuthenticated = False 76 + { currentTime = Time.millisToPosix flags.initialTime 77 + , isAuthenticated = False 74 78 , isLoading = True 75 79 , navKey = key 76 80 , page = Page.fromUrl url ··· 81 85 ----------- 82 86 , authentication = Authentication.initialModel 83 87 , backdrop = Backdrop.initialModel 88 + , queue = Queue.initialModel 84 89 , sources = Sources.initialModel 85 90 , tracks = Tracks.initialModel 86 91 } ··· 118 123 sources = 119 124 model.sources 120 125 in 121 - ( { model | sources = { sources | currentTime = time } } 126 + ( { model 127 + | currentTime = time 128 + , sources = { sources | currentTime = time } 129 + } 122 130 , Cmd.none 123 131 ) 124 132 ··· 230 238 , msg = sub 231 239 } 232 240 241 + QueueMsg sub -> 242 + updateChild 243 + { mapCmd = QueueMsg 244 + , mapModel = \child -> { model | queue = child } 245 + , update = Queue.update 246 + } 247 + { model = model.queue 248 + , msg = sub 249 + } 250 + 233 251 SourcesMsg sub -> 234 252 updateChild 235 253 { mapCmd = SourcesMsg ··· 251 269 } 252 270 253 271 ----------------------------------------- 272 + -- Children, Pt. 2 273 + ----------------------------------------- 274 + Core.ActiveQueueItemChanged maybeQueueItem -> 275 + let 276 + nowPlaying = 277 + Maybe.map .identifiedTrack maybeQueueItem 278 + 279 + portCmd = 280 + maybeQueueItem 281 + |> Maybe.map .identifiedTrack 282 + |> Maybe.map 283 + (UI.Queue.Common.makeEngineItem 284 + model.currentTime 285 + model.sources.collection 286 + ) 287 + |> Ports.activeQueueItemChanged 288 + in 289 + model 290 + |> update (TracksMsg <| Tracks.SetNowPlaying nowPlaying) 291 + |> R2.addCmd portCmd 292 + 293 + Core.FillQueue -> 294 + model.tracks.collection.harvested 295 + |> Queue.Fill model.currentTime 296 + |> QueueMsg 297 + |> (\msg_ -> update msg_ model) 298 + 299 + ----------------------------------------- 254 300 -- Import / Export 255 301 ----------------------------------------- 256 302 Export -> ··· 328 374 translateReply : Reply -> Msg 329 375 translateReply reply = 330 376 case reply of 331 - AddSourceToCollection source -> 377 + Reply.ActiveQueueItemChanged m -> 378 + Core.ActiveQueueItemChanged m 379 + 380 + Reply.AddSourceToCollection source -> 332 381 SourcesMsg (Sources.AddToCollection source) 333 382 334 - Chill -> 383 + Reply.Chill -> 335 384 Bypass 336 385 337 - GoToPage page -> 386 + Reply.FillQueue -> 387 + Core.FillQueue 388 + 389 + Reply.GoToPage page -> 338 390 ChangeUrlUsingPage page 391 + 392 + Reply.PlayTrack identifiedTrack -> 393 + QueueMsg (Queue.InjectFirstAndPlay identifiedTrack) 339 394 340 395 Reply.ProcessSources -> 341 396 Core.ProcessSources 342 397 343 398 Reply.RemoveTracksWithSourceId sourceId -> 344 399 TracksMsg (Tracks.RemoveBySourceId sourceId) 400 + 401 + Reply.ResetQueue -> 402 + QueueMsg Queue.Reset 403 + 404 + Reply.ShiftQueue -> 405 + QueueMsg Queue.Shift 345 406 346 407 Reply.SaveEnclosedUserData -> 347 408 Core.SaveEnclosedUserData ··· 496 557 Page.NotFound -> 497 558 -- TODO 498 559 UI.Kit.receptacle [ text "Page not found." ] 560 + 561 + Page.Queue _ -> 562 + -- TODO 563 + nothing 499 564 500 565 Page.Settings subPage -> 501 566 Settings.view subPage model
+14 -2
src/Applications/UI/Core.elm
··· 5 5 import Browser.Navigation as Nav 6 6 import File exposing (File) 7 7 import Json.Encode as Json 8 + import Queue 8 9 import Time 9 10 import UI.Authentication as Authentication 10 11 import UI.Backdrop as Backdrop 11 12 import UI.Page exposing (Page) 13 + import UI.Queue.Core as Queue 12 14 import UI.Sources as Sources 13 15 import UI.Tracks.Core as Tracks 14 16 import Url exposing (Url) ··· 19 21 20 22 21 23 type alias Flags = 22 - { viewport : Viewport } 24 + { initialTime : Int 25 + , viewport : Viewport 26 + } 23 27 24 28 25 29 ··· 27 31 28 32 29 33 type alias Model = 30 - { isAuthenticated : Bool 34 + { currentTime : Time.Posix 35 + , isAuthenticated : Bool 31 36 , isLoading : Bool 32 37 , navKey : Nav.Key 33 38 , page : Page ··· 39 44 ----------------------------------------- 40 45 , authentication : Authentication.Model 41 46 , backdrop : Backdrop.Model 47 + , queue : Queue.Model 42 48 , sources : Sources.Model 43 49 , tracks : Tracks.Model 44 50 } ··· 74 80 ----------------------------------------- 75 81 | AuthenticationMsg Authentication.Msg 76 82 | BackdropMsg Backdrop.Msg 83 + | QueueMsg Queue.Msg 77 84 | SourcesMsg Sources.Msg 78 85 | TracksMsg Tracks.Msg 86 + ----------------------------------------- 87 + -- Children, Pt. 2 88 + ----------------------------------------- 89 + | ActiveQueueItemChanged (Maybe Queue.Item) 90 + | FillQueue 79 91 ----------------------------------------- 80 92 -- Import / Export 81 93 -----------------------------------------
+20
src/Applications/UI/Page.elm
··· 1 1 module UI.Page exposing (Page(..), fromUrl, sameBase, toString) 2 2 3 + import UI.Queue.Page as Queue 3 4 import UI.Settings.Page as Settings 4 5 import UI.Sources.Page as Sources 5 6 import Url exposing (Url) ··· 12 13 13 14 type Page 14 15 = Index 16 + | Queue Queue.Page 15 17 | Settings Settings.Page 16 18 | Sources Sources.Page 17 19 -- ··· 39 41 "/404" 40 42 41 43 ----------------------------------------- 44 + -- Queue 45 + ----------------------------------------- 46 + Queue Queue.History -> 47 + "/queue/history" 48 + 49 + Queue Queue.Index -> 50 + "/queue" 51 + 52 + ----------------------------------------- 42 53 -- Settings 43 54 ----------------------------------------- 44 55 Settings Settings.ImportExport -> ··· 62 73 sameBase : Page -> Page -> Bool 63 74 sameBase a b = 64 75 case ( a, b ) of 76 + ( Queue _, Queue _ ) -> 77 + True 78 + 65 79 ( Settings _, Settings _ ) -> 66 80 True 67 81 ··· 81 95 oneOf 82 96 [ map Index top 83 97 , map NotFound (s "404") 98 + 99 + ----------------------------------------- 100 + -- Queue 101 + ----------------------------------------- 102 + , map (Queue Queue.History) (s "queue" </> s "history") 103 + , map (Queue Queue.Index) (s "queue") 84 104 85 105 ----------------------------------------- 86 106 -- Settings
+35 -1
src/Applications/UI/Ports.elm
··· 1 - port module UI.Ports exposing (fromBrain, giveBrain, nudgeBrain, toBrain) 1 + port module UI.Ports exposing (activeQueueItemChanged, activeQueueItemEnded, fromBrain, giveBrain, nudgeBrain, pause, play, seek, setDuration, setIsLoading, setIsPlaying, setStalled, toBrain, toggleRepeat, unstall) 2 2 3 3 import Alien 4 4 import Json.Encode as Json 5 + import Queue 5 6 6 7 7 8 8 9 -- 📣 10 + 11 + 12 + port activeQueueItemChanged : Maybe Queue.EngineItem -> Cmd msg 13 + 14 + 15 + port pause : () -> Cmd msg 16 + 17 + 18 + port play : () -> Cmd msg 19 + 20 + 21 + port seek : Float -> Cmd msg 22 + 23 + 24 + port unstall : () -> Cmd msg 9 25 10 26 11 27 port toBrain : Alien.Event -> Cmd msg 12 28 13 29 30 + port toggleRepeat : Bool -> Cmd msg 31 + 32 + 14 33 15 34 -- 📰 16 35 17 36 37 + port activeQueueItemEnded : (() -> msg) -> Sub msg 38 + 39 + 18 40 port fromBrain : (Alien.Event -> msg) -> Sub msg 41 + 42 + 43 + port setDuration : (Float -> msg) -> Sub msg 44 + 45 + 46 + port setIsLoading : (Bool -> msg) -> Sub msg 47 + 48 + 49 + port setIsPlaying : (Bool -> msg) -> Sub msg 50 + 51 + 52 + port setStalled : (Bool -> msg) -> Sub msg 19 53 20 54 21 55
+212
src/Applications/UI/Queue.elm
··· 1 + module UI.Queue exposing (initialModel, update) 2 + 3 + import List.Extra as List 4 + import Queue exposing (..) 5 + import Replying exposing (R3D3) 6 + import Return3 as R3 7 + import Time 8 + import Tracks exposing (IdentifiedTrack) 9 + import UI.Ports as Ports 10 + import UI.Queue.Common exposing (makeItem) 11 + import UI.Queue.Core exposing (..) 12 + import UI.Queue.Fill as Fill 13 + import UI.Reply exposing (Reply(..)) 14 + 15 + 16 + 17 + -- 🌳 18 + 19 + 20 + initialModel : Model 21 + initialModel = 22 + { activeItem = Nothing 23 + , future = [] 24 + , ignored = [] 25 + , past = [] 26 + 27 + -- 28 + , repeat = False 29 + , shuffle = False 30 + } 31 + 32 + 33 + 34 + -- 📣 35 + 36 + 37 + update : Msg -> Model -> R3D3 Model Msg Reply 38 + update msg model = 39 + case msg of 40 + ------------------------------------ 41 + -- Combos 42 + ------------------------------------ 43 + InjectFirstAndPlay identifiedTrack -> 44 + let 45 + ( a, b, _ ) = 46 + update (InjectFirst [ identifiedTrack ]) model 47 + 48 + ( x, y, z ) = 49 + update Shift a 50 + in 51 + ( x, Cmd.batch [ b, y ], z ) 52 + 53 + ------------------------------------ 54 + -- Future 55 + ------------------------------------ 56 + -- # InjectFirst 57 + -- > Add an item in front of the queue. 58 + -- 59 + InjectFirst identifiedTracks -> 60 + let 61 + ( items, tracks ) = 62 + ( List.map (makeItem True) identifiedTracks 63 + , List.map Tuple.second identifiedTracks 64 + ) 65 + 66 + cleanedFuture = 67 + List.foldl 68 + (\track future -> 69 + Fill.cleanAutoGenerated model.shuffle track.id future 70 + ) 71 + model.future 72 + tracks 73 + in 74 + ( { model | future = items ++ cleanedFuture } 75 + , Cmd.none 76 + , Just [ FillQueue ] 77 + ) 78 + 79 + -- # InjectLast 80 + -- > Add an item after the last manual entry 81 + -- (ie. after the last injected item). 82 + -- 83 + InjectLast identifiedTracks -> 84 + let 85 + ( items, tracks ) = 86 + ( List.map (makeItem True) identifiedTracks 87 + , List.map Tuple.second identifiedTracks 88 + ) 89 + 90 + cleanedFuture = 91 + List.foldl 92 + (\track future -> 93 + Fill.cleanAutoGenerated model.shuffle track.id future 94 + ) 95 + model.future 96 + tracks 97 + 98 + manualItems = 99 + cleanedFuture 100 + |> List.filter (.manualEntry >> (==) True) 101 + |> List.length 102 + in 103 + ( { model 104 + | future = 105 + [] 106 + ++ List.take manualItems cleanedFuture 107 + ++ items 108 + ++ List.drop manualItems cleanedFuture 109 + } 110 + , Cmd.none 111 + , Just [ FillQueue ] 112 + ) 113 + 114 + ----------------------------------------- 115 + -- Position 116 + ----------------------------------------- 117 + -- # Rewind 118 + -- > Put the next item in the queue as the current one. 119 + -- 120 + Rewind -> 121 + changeActiveItem 122 + (List.last model.past) 123 + { model 124 + | future = 125 + model.activeItem 126 + |> Maybe.map (\item -> item :: model.future) 127 + |> Maybe.withDefault model.future 128 + , past = 129 + model.past 130 + |> List.init 131 + |> Maybe.withDefault [] 132 + } 133 + 134 + -- # Shift 135 + -- > Put the next item in the queue as the current one. 136 + -- 137 + Shift -> 138 + changeActiveItem 139 + (List.head model.future) 140 + { model 141 + | future = 142 + model.future 143 + |> List.drop 1 144 + , past = 145 + model.activeItem 146 + |> Maybe.map List.singleton 147 + |> Maybe.map (List.append model.past) 148 + |> Maybe.withDefault model.past 149 + } 150 + 151 + ------------------------------------ 152 + -- Contents 153 + ------------------------------------ 154 + -- # Fill 155 + -- > Fill the queue with items. 156 + -- 157 + Fill timestamp tracks -> 158 + ( fillQueue timestamp tracks model, Cmd.none, Nothing ) 159 + 160 + -- # Reset 161 + -- > Renew the queue, meaning that the auto-generated items in the queue 162 + -- are removed and new items are added. 163 + -- 164 + Reset -> 165 + let 166 + newFuture = 167 + List.filter (.manualEntry >> (==) True) model.future 168 + in 169 + ( { model | future = newFuture, ignored = [] } 170 + , Cmd.none 171 + , Just [ FillQueue ] 172 + ) 173 + 174 + 175 + 176 + -- 📣 ░░ COMMON 177 + 178 + 179 + changeActiveItem : Maybe Item -> Model -> R3D3 Model Msg Reply 180 + changeActiveItem maybeItem model = 181 + ( { model | activeItem = maybeItem } 182 + , Cmd.none 183 + , Just [ ActiveQueueItemChanged maybeItem, FillQueue ] 184 + ) 185 + 186 + 187 + fillQueue : Time.Posix -> List IdentifiedTrack -> Model -> Model 188 + fillQueue timestamp availableTracks model = 189 + let 190 + nonMissingTracks = 191 + List.filter 192 + (Tuple.second >> .id >> (/=) Tracks.missingId) 193 + availableTracks 194 + in 195 + model 196 + |> (\m -> 197 + -- Empty the ignored list when we are ignoring all the tracks 198 + if List.length model.ignored == List.length nonMissingTracks then 199 + { m | ignored = [] } 200 + 201 + else 202 + m 203 + ) 204 + |> (\m -> 205 + -- Fill using the appropiate method 206 + case m.shuffle of 207 + False -> 208 + { m | future = Fill.ordered timestamp nonMissingTracks m } 209 + 210 + True -> 211 + { m | future = Fill.shuffled timestamp nonMissingTracks m } 212 + )
+45
src/Applications/UI/Queue/Common.elm
··· 1 + module UI.Queue.Common exposing (makeEngineItem, makeItem) 2 + 3 + import List.Extra as List 4 + import Queue exposing (..) 5 + import Sources exposing (Source) 6 + import Sources.Processing exposing (HttpMethod(..)) 7 + import Sources.Services 8 + import Time 9 + import Tracks exposing (IdentifiedTrack, Track) 10 + 11 + 12 + 13 + -- 🔱 14 + 15 + 16 + makeEngineItem : Time.Posix -> List Source -> IdentifiedTrack -> EngineItem 17 + makeEngineItem timestamp sources ( _, track ) = 18 + { track = track 19 + , url = 20 + sources 21 + |> List.find (.id >> (==) track.sourceId) 22 + |> Maybe.map (makeTrackUrl timestamp track) 23 + |> Maybe.withDefault "<missing-source>" 24 + } 25 + 26 + 27 + makeItem : Bool -> IdentifiedTrack -> Item 28 + makeItem isManualEntry identifiedTrack = 29 + { manualEntry = isManualEntry 30 + , identifiedTrack = identifiedTrack 31 + } 32 + 33 + 34 + 35 + -- ㊙️ 36 + 37 + 38 + makeTrackUrl : Time.Posix -> Track -> Source -> String 39 + makeTrackUrl timestamp track source = 40 + Sources.Services.makeTrackUrl 41 + source.service 42 + timestamp 43 + source.data 44 + Get 45 + track.path
+47
src/Applications/UI/Queue/Core.elm
··· 1 + module UI.Queue.Core exposing (Model, Msg(..)) 2 + 3 + import Queue exposing (..) 4 + import Time 5 + import Tracks exposing (IdentifiedTrack) 6 + 7 + 8 + 9 + -- 🌳 10 + 11 + 12 + type alias Model = 13 + { activeItem : Maybe Item 14 + , future : List Item 15 + , ignored : List Item 16 + , past : List Item 17 + 18 + -- 19 + , repeat : Bool 20 + , shuffle : Bool 21 + } 22 + 23 + 24 + 25 + -- 📣 26 + 27 + 28 + type Msg 29 + = ------------------------------------ 30 + -- Combos 31 + ------------------------------------ 32 + InjectFirstAndPlay IdentifiedTrack 33 + ------------------------------------ 34 + -- Future 35 + ------------------------------------ 36 + | InjectFirst (List IdentifiedTrack) 37 + | InjectLast (List IdentifiedTrack) 38 + ------------------------------------ 39 + -- Position 40 + ------------------------------------ 41 + | Rewind 42 + | Shift 43 + ------------------------------------ 44 + -- Contents 45 + ------------------------------------ 46 + | Reset 47 + | Fill Time.Posix (List IdentifiedTrack)
+260
src/Applications/UI/Queue/Fill.elm
··· 1 + module UI.Queue.Fill exposing (cleanAutoGenerated, ordered, shuffled) 2 + 3 + {-| These functions will return a new list for the `future` property. 4 + -} 5 + 6 + import List.Extra as List 7 + import Maybe.Ext as Maybe 8 + import Maybe.Extra as Maybe 9 + import Queue exposing (Item) 10 + import Random exposing (Generator, Seed) 11 + import Time 12 + import Tracks exposing (IdentifiedTrack) 13 + import UI.Queue.Common exposing (makeItem) 14 + import UI.Queue.Core exposing (Model) 15 + 16 + 17 + 18 + -- 🔱 19 + 20 + 21 + cleanAutoGenerated : Bool -> String -> List Item -> List Item 22 + cleanAutoGenerated shuffle trackId future = 23 + case shuffle of 24 + True -> 25 + List.filterNot 26 + (\i -> i.manualEntry == False && itemTrackId i == trackId) 27 + future 28 + 29 + False -> 30 + future 31 + 32 + 33 + 34 + -- 🔱 ░░ ORDERED 35 + 36 + 37 + ordered : Time.Posix -> List IdentifiedTrack -> Model -> List Item 38 + ordered _ rawTracks model = 39 + let 40 + tracks = 41 + purifyTracksList model.ignored rawTracks 42 + 43 + manualEntries = 44 + List.filter (.manualEntry >> (==) True) model.future 45 + 46 + remaining = 47 + max (queueLength - List.length manualEntries) 0 48 + 49 + focus = 50 + Maybe.preferFirst (List.last manualEntries) model.activeItem 51 + in 52 + case focus of 53 + Just item -> 54 + tracks 55 + |> List.findIndex (indexFinder item.identifiedTrack) 56 + |> Maybe.map (\idx -> List.drop (idx + 1) tracks) 57 + |> Maybe.withDefault tracks 58 + |> List.take remaining 59 + |> (\a -> 60 + let 61 + actualRemaining = 62 + remaining - List.length a 63 + 64 + n = 65 + tracks 66 + |> List.findIndex (indexFinder item.identifiedTrack) 67 + |> Maybe.withDefault (List.length tracks) 68 + in 69 + a ++ List.take (min n actualRemaining) tracks 70 + ) 71 + |> List.map (makeItem False) 72 + |> List.append manualEntries 73 + 74 + Nothing -> 75 + tracks 76 + |> List.take remaining 77 + |> List.map (makeItem False) 78 + |> List.append manualEntries 79 + 80 + 81 + 82 + -- 🔱 ░░ SHUFFLED 83 + 84 + 85 + shuffled : Time.Posix -> List IdentifiedTrack -> Model -> List Item 86 + shuffled timestamp rawTracks model = 87 + let 88 + tracks = 89 + purifyTracksList model.ignored rawTracks 90 + 91 + amountOfTracks = 92 + List.length tracks 93 + 94 + generator = 95 + Random.int 0 (amountOfTracks - 1) 96 + 97 + ( pastIds, futureIds, activeId ) = 98 + ( List.map itemTrackId model.past 99 + , List.map itemTrackId model.future 100 + , Maybe.map itemTrackId model.activeItem 101 + ) 102 + 103 + usedIndexes = 104 + collectIndexes 105 + tracks 106 + [ \( _, t ) -> List.member t.id pastIds 107 + , \( _, t ) -> List.member t.id futureIds 108 + , \( _, t ) -> Just t.id == activeId 109 + ] 110 + 111 + usedIndexes_ = 112 + let 113 + isUsedUp = 114 + List.length usedIndexes >= amountOfTracks 115 + 116 + hasNoFuture = 117 + List.isEmpty model.future 118 + in 119 + if isUsedUp && hasNoFuture && amountOfTracks > 1 then 120 + case amountOfTracks > 1 of 121 + True -> 122 + collectIndexes tracks [ \( _, t ) -> Just t.id == activeId ] 123 + 124 + False -> 125 + [] 126 + 127 + else 128 + usedIndexes 129 + 130 + ( toAmount, maxAmount ) = 131 + ( max (queueLength - List.length model.future) 0 132 + , max (amountOfTracks - List.length usedIndexes_) 0 133 + ) 134 + 135 + howMany = 136 + min toAmount maxAmount 137 + in 138 + if howMany > 0 then 139 + timestamp 140 + |> Time.toMillis Time.utc 141 + |> Random.initialSeed 142 + |> generateIndexes generator howMany usedIndexes_ [] 143 + |> List.map (\idx -> List.getAt idx tracks) 144 + |> Maybe.values 145 + |> List.map (makeItem False) 146 + |> List.append model.future 147 + 148 + else 149 + model.future 150 + 151 + 152 + 153 + -- ㊙️ 154 + 155 + 156 + collectIndexes : List IdentifiedTrack -> List (IdentifiedTrack -> Bool) -> List Int 157 + collectIndexes tracks audits = 158 + List.indexedFoldl (collector audits) [] tracks 159 + 160 + 161 + collector : List (IdentifiedTrack -> Bool) -> Int -> IdentifiedTrack -> List Int -> List Int 162 + collector audits idx track acc = 163 + case List.foldl (auditor track) False audits of 164 + True -> 165 + idx :: acc 166 + 167 + False -> 168 + acc 169 + 170 + 171 + auditor : IdentifiedTrack -> (IdentifiedTrack -> Bool) -> Bool -> Bool 172 + auditor track audit acc = 173 + if acc == True then 174 + acc 175 + 176 + else 177 + audit track 178 + 179 + 180 + {-| Generated random indexes. 181 + 182 + `squirrel` = accumulator, ie. collected indexes 183 + 184 + -} 185 + generateIndexes : Generator Int -> Int -> List Int -> List Int -> Seed -> List Int 186 + generateIndexes generator howMany usedIndexes squirrel seed = 187 + let 188 + ( index, newSeed ) = 189 + Random.step generator seed 190 + 191 + newSquirrel = 192 + if List.member index usedIndexes then 193 + squirrel 194 + 195 + else if List.member index squirrel then 196 + squirrel 197 + 198 + else 199 + index :: squirrel 200 + in 201 + if List.length newSquirrel < howMany then 202 + generateIndexes generator howMany usedIndexes newSquirrel newSeed 203 + 204 + else 205 + newSquirrel 206 + 207 + 208 + 209 + -- PURIFY 210 + 211 + 212 + purifyTracksList : List Item -> List IdentifiedTrack -> List IdentifiedTrack 213 + purifyTracksList ignored tracks = 214 + let 215 + ignoredTrackIds = 216 + List.map itemTrackId ignored 217 + in 218 + tracks 219 + |> List.foldr purifyTracksListReducer ( [], ignoredTrackIds ) 220 + |> Tuple.first 221 + 222 + 223 + purifyTracksListReducer : 224 + IdentifiedTrack 225 + -> ( List IdentifiedTrack, List String ) 226 + -> ( List IdentifiedTrack, List String ) 227 + purifyTracksListReducer identifiedTrack ( collection, ignored ) = 228 + let 229 + trackId = 230 + (Tuple.second >> .id) identifiedTrack 231 + in 232 + case List.findIndex ((==) trackId) ignored of 233 + Just idx -> 234 + ( collection, List.removeAt idx ignored ) 235 + 236 + Nothing -> 237 + ( identifiedTrack :: collection, ignored ) 238 + 239 + 240 + 241 + -- COMMON 242 + 243 + 244 + indexFinder : IdentifiedTrack -> IdentifiedTrack -> Bool 245 + indexFinder = 246 + Tracks.isNowPlaying 247 + 248 + 249 + itemTrackId : Item -> String 250 + itemTrackId = 251 + .identifiedTrack >> Tuple.second >> .id 252 + 253 + 254 + 255 + -- CONSTANTS 256 + 257 + 258 + queueLength : Int 259 + queueLength = 260 + 30
+8
src/Applications/UI/Queue/Page.elm
··· 1 + module UI.Queue.Page exposing (Page(..)) 2 + 3 + -- 🌳 4 + 5 + 6 + type Page 7 + = Index 8 + | History
+8 -1
src/Applications/UI/Reply.elm
··· 2 2 3 3 import Alien 4 4 import Json.Decode as Json 5 + import Queue 5 6 import Sources exposing (Source) 7 + import Tracks exposing (IdentifiedTrack) 6 8 import UI.Page exposing (Page) 7 9 8 10 ··· 11 13 12 14 13 15 type Reply 14 - = AddSourceToCollection Source 16 + = ActiveQueueItemChanged (Maybe Queue.Item) 17 + | AddSourceToCollection Source 15 18 | Chill 19 + | FillQueue 16 20 | GoToPage Page 21 + | PlayTrack IdentifiedTrack 17 22 | ProcessSources 18 23 | RemoveTracksWithSourceId String 24 + | ResetQueue 25 + | ShiftQueue 19 26 | SaveEnclosedUserData 20 27 | SaveFavourites 21 28 | SaveSources
+30 -7
src/Applications/UI/Tracks.elm
··· 17 17 import Material.Icons.Av as Icons 18 18 import Material.Icons.Content as Icons 19 19 import Material.Icons.Editor as Icons 20 + import Maybe.Extra as Maybe 20 21 import Replying exposing (R3D3) 21 22 import Return2 22 23 import Return3 ··· 63 64 Bypass -> 64 65 Return3.withNothing model 65 66 67 + Play identifiedTrack -> 68 + ( model, Cmd.none, Just [ PlayTrack identifiedTrack ] ) 69 + 66 70 InfiniteListMsg infiniteList -> 67 71 Return3.withNothing { model | infiniteList = infiniteList } 68 72 69 73 SetEnabledSourceIds sourceIds -> 70 74 Return3.withNothing { model | enabledSourceIds = sourceIds } 75 + 76 + SetNowPlaying maybeIdentifiedTrack -> 77 + reviseCollection harvest { model | nowPlaying = maybeIdentifiedTrack } 71 78 72 79 ----------------------------------------- 73 80 -- Collection ··· 89 96 90 97 -- # Remove 91 98 -- > Remove tracks from the collection. 99 + -- 92 100 RemoveByPaths json -> 93 101 let 94 102 decoder = ··· 184 192 let 185 193 modelWithNewCollection = 186 194 { model | collection = newCollection } 195 + 196 + oldHarvest = 197 + List.map (Tuple.second >> .id) model.collection.harvested 198 + 199 + newHarvest = 200 + List.map (Tuple.second >> .id) newCollection.harvested 187 201 in 188 - if model.collection.untouched /= newCollection.untouched then 189 - ( modelWithNewCollection 190 - , Cmd.none 191 - , Just [ SaveTracks ] 192 - ) 202 + ( modelWithNewCollection 203 + , Cmd.none 204 + , (Just << Maybe.values) 205 + [ if model.collection.untouched /= newCollection.untouched then 206 + Just SaveTracks 193 207 194 - else 195 - Return3.withNothing modelWithNewCollection 208 + else 209 + Nothing 210 + 211 + -- 212 + , if oldHarvest /= newHarvest then 213 + Just ResetQueue 214 + 215 + else 216 + Nothing 217 + ] 218 + ) 196 219 197 220 198 221 reviseCollection : (Parcel -> Parcel) -> Model -> R3D3 Model Msg Reply
+2
src/Applications/UI/Tracks/Core.elm
··· 31 31 type Msg 32 32 = Bypass 33 33 | InfiniteListMsg InfiniteList.Model 34 + | Play IdentifiedTrack 34 35 | SetEnabledSourceIds (List String) 36 + | SetNowPlaying (Maybe IdentifiedTrack) 35 37 ----------------------------------------- 36 38 -- Collection 37 39 -----------------------------------------
+6 -2
src/Applications/UI/Tracks/Scene/List.elm
··· 9 9 import Html.Attributes as UnstyledHtmlAttributes 10 10 import Html.Styled as Html exposing (Html, text) 11 11 import Html.Styled.Attributes exposing (css, fromUnstyled) 12 + import Html.Styled.Events exposing (onDoubleClick) 12 13 import Html.Styled.Lazy 13 14 import InfiniteList 14 15 import Material.Icons.Navigation as Icons ··· 133 134 ] 134 135 ] 135 136 [ T.lh_title 136 - , T.ph2 137 137 , T.pv1 138 138 , T.relative 139 139 140 140 -- 141 + , ifThenElse (pos == First) T.pl3 T.pl2 142 + , ifThenElse (pos == Last) T.pr3 T.pr2 141 143 , ifThenElse (pos == First) "" T.pointer 142 144 ] 143 145 [ brick ··· 306 308 Html.toUnstyled <| 307 309 slab 308 310 Html.li 309 - [ css (rowStyles idx identifiers) ] 311 + [ css (rowStyles idx identifiers) 312 + , onDoubleClick (Play ( identifiers, track )) 313 + ] 310 314 [ T.dt_row 311 315 312 316 --
+18 -17
src/Javascript/index.js
··· 9 9 const app = Elm.UI.init({ 10 10 node: document.getElementById("elm"), 11 11 flags: { 12 + initialTime: Date.now(), 12 13 viewport: { 13 14 height: window.innerHeight, 14 15 width: window.innerWidth ··· 43 44 } 44 45 45 46 46 - // app.ports.activeQueueItemChanged.subscribe(item => { 47 - // const timestampInMilliseconds = Date.now() 48 - // 49 - // orchestrion.activeQueueItem = item 50 - // orchestrion.audio = null 51 - // 52 - // removeOlderAudioElements(timestampInMilliseconds) 53 - // 54 - // if (item) { 55 - // insertTrack(orchestrion, item) 56 - // } else { 57 - // app.ports.setIsPlaying.send(false) 58 - // setProgressBarWidth(0) 59 - // } 60 - // }) 61 - // 62 - // 47 + app.ports.activeQueueItemChanged.subscribe(item => { 48 + const timestampInMilliseconds = Date.now() 49 + 50 + orchestrion.activeQueueItem = item 51 + orchestrion.audio = null 52 + 53 + removeOlderAudioElements(timestampInMilliseconds) 54 + 55 + if (item) { 56 + insertTrack(orchestrion, item) 57 + } else { 58 + app.ports.setIsPlaying.send(false) 59 + setProgressBarWidth(0) 60 + } 61 + }) 62 + 63 + 63 64 // app.ports.play.subscribe(_ => { 64 65 // if (orchestrion.audio) orchestrion.audio.play() 65 66 // })
+19
src/Library/Queue.elm
··· 1 + module Queue exposing (EngineItem, Item) 2 + 3 + import Tracks exposing (IdentifiedTrack, Track) 4 + 5 + 6 + 7 + -- 🌳 8 + 9 + 10 + type alias Item = 11 + { manualEntry : Bool 12 + , identifiedTrack : IdentifiedTrack 13 + } 14 + 15 + 16 + type alias EngineItem = 17 + { track : Track 18 + , url : String 19 + }
+6 -1
src/Library/Tracks.elm
··· 1 - module Tracks exposing (Collection, CollectionDependencies, Favourite, IdentifiedTrack, Identifiers, Parcel, SortBy(..), SortDirection(..), Tags, Track, emptyCollection, emptyIdentifiedTrack, emptyTags, emptyTrack, makeTrack, missingId) 1 + module Tracks exposing (Collection, CollectionDependencies, Favourite, IdentifiedTrack, Identifiers, Parcel, SortBy(..), SortDirection(..), Tags, Track, emptyCollection, emptyIdentifiedTrack, emptyTags, emptyTrack, isNowPlaying, makeTrack, missingId) 2 2 3 3 import Base64 4 4 import Bytes.Encode ··· 158 158 , arranged = [] 159 159 , harvested = [] 160 160 } 161 + 162 + 163 + isNowPlaying : IdentifiedTrack -> IdentifiedTrack -> Bool 164 + isNowPlaying ( a, b ) ( x, y ) = 165 + a.indexInPlaylist == x.indexInPlaylist && b == y 161 166 162 167 163 168 makeTrack : String -> ( String, Tags ) -> Track