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 tracks state

+1361 -1448
+37 -22
src/Applications/UI.elm
··· 11 11 import Debouncer.Basic as Debouncer 12 12 import Dict 13 13 import Equalizer 14 + import InfiniteList 14 15 import Keyboard 15 16 import LastFm 16 17 import Maybe.Extra as Maybe ··· 18 19 import Playlists.Encoding as Playlists 19 20 import Queue 20 21 import Return 21 - import Return3 22 22 import Sources 23 23 import Sources.Encoding as Sources 24 24 import Task ··· 49 49 import UI.Sources.ContextMenu as Sources 50 50 import UI.Sources.Form 51 51 import UI.Sources.State as Sources 52 - import UI.Tracks as Tracks 53 52 import UI.Tracks.ContextMenu as Tracks 54 53 import UI.Tracks.State as Tracks 54 + import UI.Tracks.Types as Tracks 55 55 import UI.Types as UI exposing (..) 56 56 import UI.User.State.Export as User 57 57 import UI.User.State.Import as User ··· 160 160 , newPlaylistContext = Nothing 161 161 , playlists = [] 162 162 , playlistToActivate = Nothing 163 + , selectedPlaylist = Nothing 163 164 164 165 ----------------------------------------- 165 166 -- Queue ··· 184 185 , sourceForm = UI.Sources.Form.initialModel 185 186 186 187 ----------------------------------------- 187 - -- 🦉 Nested 188 + -- Tracks 188 189 ----------------------------------------- 189 - , authentication = Authentication.initialModel url 190 + , cachedTracks = [] 191 + , cachedTracksOnly = False 192 + , cachingTracksInProgress = [] 193 + , favourites = [] 194 + , favouritesOnly = False 195 + , grouping = Nothing 196 + , hideDuplicates = False 197 + , scene = Tracks.List 198 + , searchResults = Nothing 199 + , searchTerm = Nothing 200 + , selectedTrackIndexes = [] 201 + , sortBy = Tracks.Artist 202 + , sortDirection = Tracks.Asc 203 + , tracks = Tracks.emptyCollection 204 + 205 + -- List scene 206 + ------------- 207 + , infiniteList = InfiniteList.init 190 208 191 209 ----------------------------------------- 192 - -- Children (TODO) 210 + -- 🦉 Nested 193 211 ----------------------------------------- 194 - , tracks = Tracks.initialModel 212 + , authentication = Authentication.initialModel url 195 213 } 196 214 |> Routing.transition page 197 215 |> Return.command ··· 338 356 Interface.stoppedDragging 339 357 340 358 UI.ToggleLoadingScreen a -> 341 - Interface.toggleLoadingScreen a 359 + Common.toggleLoadingScreen a 342 360 343 361 ----------------------------------------- 344 362 -- Playlists ··· 358 376 DeletePlaylist a -> 359 377 Playlists.delete a 360 378 379 + DeselectPlaylist -> 380 + Playlists.deselect 381 + 361 382 ModifyPlaylist -> 362 383 Playlists.modify 384 + 385 + MoveTrackInSelectedPlaylist a -> 386 + Playlists.moveTrackInSelectedPlaylist a 387 + 388 + SelectPlaylist a -> 389 + Playlists.select a 363 390 364 391 SetPlaylistCreationContext a -> 365 392 Playlists.setCreationContext a ··· 442 469 SourcesMsg a -> 443 470 Sources.update a 444 471 472 + TracksMsg a -> 473 + Tracks.update a 474 + 445 475 ----------------------------------------- 446 476 -- 📭 Other 447 477 ----------------------------------------- ··· 450 480 451 481 SetIsOnline a -> 452 482 Other.setIsOnline a 453 - 454 - ----------------------------------------- 455 - -- Children (TODO) 456 - ----------------------------------------- 457 - TracksMsg sub -> 458 - \model -> 459 - Return3.wieldNested 460 - Reply.translate 461 - { mapCmd = TracksMsg 462 - , mapModel = \child -> { model | tracks = child } 463 - , update = Tracks.update 464 - } 465 - { model = model.tracks 466 - , msg = sub 467 - } 468 483 469 484 470 485
+1 -1
src/Applications/UI/Alien.elm
··· 6 6 import Notifications 7 7 import UI.Authentication.Types as Authentication 8 8 import UI.Sources.Types as Sources 9 - import UI.Tracks as Tracks 9 + import UI.Tracks.Types as Tracks 10 10 import UI.Types exposing (..) 11 11 import User.Layer exposing (..) 12 12
+1 -1
src/Applications/UI/Audio/State.elm
··· 54 54 setDuration duration model = 55 55 let 56 56 cmd = 57 - case model.tracks.nowPlaying of 57 + case Maybe.map .identifiedTrack model.nowPlaying of 58 58 Just ( _, track ) -> 59 59 LastFm.nowPlaying model.lastFm 60 60 { duration = round duration
+3 -3
src/Applications/UI/Authentication/State.elm
··· 281 281 |> Notifications.error 282 282 |> showNotificationWithModel model 283 283 |> andThen Backdrop.setDefault 284 - |> andThen (Interface.toggleLoadingScreen Off) 284 + |> andThen (Common.toggleLoadingScreen Off) 285 285 286 286 287 287 notAuthenticated : Manager ··· 357 357 |> Ports.toBrain 358 358 -- 359 359 |> Return.return model 360 - |> Return.andThen (Interface.toggleLoadingScreen On) 360 + |> Return.andThen (Common.toggleLoadingScreen On) 361 361 362 362 363 363 signInWithPassphrase : Method -> String -> Manager ··· 376 376 |> Ports.toBrain 377 377 -- 378 378 |> Return.return model 379 - |> Return.andThen (Interface.toggleLoadingScreen On) 379 + |> Return.andThen (Common.toggleLoadingScreen On) 380 380 381 381 382 382 startFlow : Manager
+34
src/Applications/UI/Common/State.elm
··· 1 1 module UI.Common.State exposing (..) 2 2 3 3 import Browser.Navigation as Nav 4 + import Common exposing (..) 4 5 import ContextMenu exposing (ContextMenu) 6 + import List.Extra as List 5 7 import Monocle.Lens as Lens exposing (Lens) 6 8 import Notifications exposing (Notification) 7 9 import Return exposing (return) 8 10 import UI.Notifications 9 11 import UI.Page as Page exposing (Page) 12 + import UI.Playlists.Directory 10 13 import UI.Reply exposing (Reply) 11 14 import UI.Types as UI exposing (Manager) 12 15 ··· 31 34 |> Return.mapCmd UI.Reply 32 35 33 36 37 + generateDirectoryPlaylists : Manager 38 + generateDirectoryPlaylists model = 39 + let 40 + nonDirectoryPlaylists = 41 + List.filterNot 42 + .autoGenerated 43 + model.playlists 44 + 45 + directoryPlaylists = 46 + UI.Playlists.Directory.generate 47 + model.sources 48 + model.tracks.untouched 49 + in 50 + [ nonDirectoryPlaylists 51 + , directoryPlaylists 52 + ] 53 + |> List.concat 54 + |> (\c -> { model | playlists = c }) 55 + |> Return.singleton 56 + 57 + 34 58 showContextMenuWithModel : UI.Model -> ContextMenu Reply -> ( UI.Model, Cmd UI.Msg ) 35 59 showContextMenuWithModel model contextMenu = 36 60 Return.singleton { model | contextMenu = Just contextMenu } ··· 47 71 showNotificationWithModel : UI.Model -> Notification Reply -> ( UI.Model, Cmd UI.Msg ) 48 72 showNotificationWithModel model notification = 49 73 showNotification notification model 74 + 75 + 76 + toggleLoadingScreen : Switch -> Manager 77 + toggleLoadingScreen switch model = 78 + case switch of 79 + On -> 80 + Return.singleton { model | isLoading = True } 81 + 82 + Off -> 83 + Return.singleton { model | isLoading = False } 50 84 51 85 52 86
+12 -23
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.Playlists.State as Playlists 10 11 import UI.Queue.State as Queue 11 - import UI.Tracks as Tracks 12 - import UI.Tracks.Scene.List 13 - import UI.Tracks.State as Tracks 12 + import UI.Tracks.Types as Tracks 14 13 import UI.Types as UI exposing (..) 15 14 import User.Layer exposing (..) 16 15 ··· 75 74 in 76 75 Queue.fill { m | playingNext = newFuture } 77 76 77 + Page.Index -> 78 + case model.scene of 79 + Tracks.List -> 80 + Playlists.moveTrackInSelectedPlaylist 81 + { to = Maybe.withDefault 0 (DnD.modelTarget d) } 82 + m 83 + 78 84 _ -> 79 85 Return.singleton m 80 86 ··· 108 114 109 115 110 116 removeTrackSelection : Manager 111 - removeTrackSelection = 112 - modifySingleton Tracks.lens (\t -> { t | selectedTrackIndexes = [] }) 117 + removeTrackSelection model = 118 + Return.singleton { model | selectedTrackIndexes = [] } 113 119 114 120 115 121 resizedWindow : ( Int, Int ) -> Manager ··· 139 145 dnd DnD.stoppedDragging notDragging 140 146 141 147 Page.Index -> 142 - case model.tracks.scene of 143 - Tracks.List -> 144 - -- TODO! 145 - DnD.stoppedDragging 146 - |> UI.Tracks.Scene.List.DragAndDropMsg 147 - |> Tracks.ListSceneMsg 148 - |> TracksMsg 149 - |> Return.performanceF notDragging 148 + dnd DnD.stoppedDragging notDragging 150 149 151 150 _ -> 152 151 Return.singleton notDragging 153 - 154 - 155 - toggleLoadingScreen : Switch -> Manager 156 - toggleLoadingScreen switch model = 157 - case switch of 158 - On -> 159 - Return.singleton { model | isLoading = True } 160 - 161 - Off -> 162 - Return.singleton { model | isLoading = False } 163 152 164 153 165 154
+55 -10
src/Applications/UI/Playlists/State.elm
··· 5 5 import Coordinates 6 6 import Html.Events.Extra.Mouse as Mouse 7 7 import Json.Encode 8 + import List.Ext as List 8 9 import List.Extra as List 9 10 import Notifications 10 11 import Playlists exposing (..) 11 12 import Playlists.Encoding as Playlists 12 13 import Return exposing (andThen, return) 13 14 import Return.Ext as Return 15 + import Tracks.Collection 14 16 import UI.Common.State as Common 15 17 import UI.Page as Page 16 18 import UI.Playlists.ContextMenu as Playlists 17 19 import UI.Playlists.Page exposing (..) 18 20 import UI.Ports as Ports 19 - import UI.Tracks as Tracks 21 + import UI.Reply as Reply 22 + import UI.Tracks.State as Tracks 23 + import UI.Tracks.Types as Tracks 20 24 import UI.Types as UI exposing (..) 25 + import UI.User.State.Export as User 21 26 22 27 23 28 ··· 26 31 27 32 activate : Playlist -> Manager 28 33 activate playlist model = 29 - playlist 30 - |> Tracks.SelectPlaylist 31 - |> TracksMsg 32 - |> Return.performanceF model 34 + model 35 + |> select playlist 33 36 |> andThen redirectToIndexPage 34 37 35 38 ··· 114 117 115 118 116 119 deactivate : Manager 117 - deactivate model = 118 - Tracks.DeselectPlaylist 119 - |> TracksMsg 120 - |> Return.performanceF model 120 + deactivate = 121 + deselect 122 + 123 + 124 + deselect : Manager 125 + deselect model = 126 + { model | selectedPlaylist = Nothing } 127 + |> Tracks.reviseCollection Tracks.Collection.arrange 128 + |> andThen User.saveEnclosedUserData 121 129 122 130 123 131 delete : { playlistName : String } -> Manager ··· 181 189 redirectToIndexPage model 182 190 183 191 192 + moveTrackInSelectedPlaylist : { to : Int } -> Manager 193 + moveTrackInSelectedPlaylist { to } model = 194 + case model.selectedPlaylist of 195 + Just playlist -> 196 + let 197 + moveParams = 198 + { from = Maybe.withDefault 0 (List.head model.selectedTrackIndexes) 199 + , to = to 200 + , amount = List.length model.selectedTrackIndexes 201 + } 202 + 203 + updatedPlaylist = 204 + { playlist | tracks = List.move moveParams playlist.tracks } 205 + 206 + updatedPlaylistCollection = 207 + List.map 208 + (\p -> ifThenElse (p.name == updatedPlaylist.name) updatedPlaylist p) 209 + model.playlists 210 + in 211 + { model 212 + | playlists = updatedPlaylistCollection 213 + , selectedPlaylist = Just updatedPlaylist 214 + } 215 + |> Tracks.reviseCollection Tracks.Collection.arrange 216 + |> andThen (Return.performance <| Reply Reply.SavePlaylists) 217 + 218 + Nothing -> 219 + Return.singleton model 220 + 221 + 184 222 save : Manager 185 223 save model = 186 224 model.playlists ··· 189 227 |> Alien.broadcast Alien.SavePlaylists 190 228 |> Ports.toBrain 191 229 |> return model 230 + 231 + 232 + select : Playlist -> Manager 233 + select playlist model = 234 + { model | selectedPlaylist = Just playlist } 235 + |> Tracks.reviseCollection Tracks.Collection.arrange 236 + |> andThen User.saveEnclosedUserData 192 237 193 238 194 239 setCreationContext : String -> Manager ··· 216 261 contextMenu = 217 262 Playlists.listMenu 218 263 playlist 219 - model.tracks.collection.identified 264 + model.tracks.identified 220 265 model.confirmation 221 266 coordinates 222 267 in
+21 -34
src/Applications/UI/Queue/State.elm
··· 14 14 import UI.Queue.ContextMenu as Queue 15 15 import UI.Queue.Fill as Fill 16 16 import UI.Queue.Types as Queue exposing (..) 17 - import UI.Tracks as Tracks 18 17 import UI.Types exposing (..) 19 18 import UI.User.State.Export as User exposing (..) 20 19 ··· 75 74 76 75 changeActiveItem : Maybe Item -> Manager 77 76 changeActiveItem maybeItem model = 78 - let 79 - nowPlaying = 80 - Maybe.map .identifiedTrack maybeItem 77 + maybeItem 78 + |> Maybe.map 79 + (.identifiedTrack >> Tuple.second) 80 + |> Maybe.map 81 + (Queue.makeEngineItem 82 + model.currentTime 83 + model.sources 84 + model.cachedTracks 85 + (if model.rememberProgress then 86 + model.progress 81 87 82 - portCmd = 83 - maybeItem 84 - |> Maybe.map 85 - (.identifiedTrack >> Tuple.second) 86 - |> Maybe.map 87 - (Queue.makeEngineItem 88 - model.currentTime 89 - model.sources 90 - model.tracks.cached 91 - (if model.rememberProgress then 92 - model.progress 93 - 94 - else 95 - Dict.empty 96 - ) 97 - ) 98 - |> Ports.activeQueueItemChanged 99 - in 100 - { model | nowPlaying = maybeItem } 101 - |> Return.performance 102 - (nowPlaying 103 - |> Tracks.SetNowPlaying 104 - |> TracksMsg 88 + else 89 + Dict.empty 90 + ) 105 91 ) 92 + |> Ports.activeQueueItemChanged 93 + |> return { model | nowPlaying = maybeItem } 106 94 |> andThen fill 107 - |> Return.command portCmd 108 95 109 96 110 97 clear : Manager ··· 116 103 fill model = 117 104 let 118 105 ( availableTracks, timestamp ) = 119 - ( model.tracks.collection.harvested 106 + ( model.tracks.harvested 120 107 , model.currentTime 121 108 ) 122 109 ··· 168 155 |> Queue.makeEngineItem 169 156 model.currentTime 170 157 model.sources 171 - model.tracks.cached 158 + model.cachedTracks 172 159 (if model.rememberProgress then 173 160 model.progress 174 161 ··· 234 221 mouseEvent.clientPos 235 222 |> Coordinates.fromTuple 236 223 |> Queue.futureMenu 237 - { cached = model.tracks.cached 238 - , cachingInProgress = model.tracks.cachingInProgress 224 + { cached = model.cachedTracks 225 + , cachingInProgress = model.cachingTracksInProgress 239 226 , itemIndex = index 240 227 } 241 228 item ··· 249 236 mouseEvent.clientPos 250 237 |> Coordinates.fromTuple 251 238 |> Queue.historyMenu 252 - { cached = model.tracks.cached 253 - , cachingInProgress = model.tracks.cachingInProgress 239 + { cached = model.cachedTracks 240 + , cachingInProgress = model.cachingTracksInProgress 254 241 } 255 242 item 256 243 |> Just
-6
src/Applications/UI/Reply.elm
··· 40 40 | ReplyViaContextMenu Reply 41 41 | ShowMoreAuthenticationOptions Coordinates 42 42 | ShowPlaylistListMenu Coordinates Playlist 43 - | ShowTracksContextMenu Coordinates { alt : Bool } (List IdentifiedTrack) 44 - | ShowTracksViewMenu Coordinates (Maybe Tracks.Grouping) 45 43 ----------------------------------------- 46 44 -- Last.fm 47 45 ----------------------------------------- ··· 65 63 | ActivatePlaylist Playlist 66 64 | AddTracksToPlaylist { playlistName : String, tracks : List PlaylistTrack } 67 65 | DeactivatePlaylist 68 - | GenerateDirectoryPlaylists 69 66 | RemoveFromSelectedPlaylist Playlist (List IdentifiedTrack) 70 67 | RemovePlaylistFromCollection { playlistName : String } 71 - | ReplacePlaylistInCollection Playlist 72 68 | RequestAssistanceForPlaylists (List IdentifiedTrack) 73 69 ----------------------------------------- 74 70 -- Queue ··· 76 72 | AddToQueue { inFront : Bool, tracks : List IdentifiedTrack } 77 73 | MoveQueueItemToFirst { itemIndex : Int } 78 74 | MoveQueueItemToLast { itemIndex : Int } 79 - | PlayTrack IdentifiedTrack 80 - | ResetQueue 81 75 | RewindQueue 82 76 | ShiftQueue 83 77 | ToggleRepeat
+29 -128
src/Applications/UI/Reply/Translate.elm
··· 43 43 import UI.Queue.Types as Queue 44 44 import UI.Reply as Reply exposing (Reply(..)) 45 45 import UI.Settings as Settings 46 + import UI.Settings.State as Settings 46 47 import UI.Sources.ContextMenu as Sources 47 48 import UI.Sources.State as Sources 48 49 import UI.Sources.Types as Sources 49 - import UI.Tracks as Tracks 50 50 import UI.Tracks.ContextMenu as Tracks 51 51 import UI.Tracks.Scene.List 52 - import UI.Tracks.State as Tracks 52 + import UI.Tracks.Types as Tracks 53 53 import UI.Types as UI exposing (..) 54 54 import Url exposing (Protocol(..)) 55 55 import Url.Ext as Url ··· 77 77 Common.changeUrlUsingPage page model 78 78 79 79 Reply.ToggleLoadingScreen a -> 80 - Interface.toggleLoadingScreen a model 80 + Common.toggleLoadingScreen a model 81 81 82 82 ----------------------------------------- 83 83 -- Audio ··· 138 138 |> Return.performanceF model 139 139 140 140 SignOut -> 141 - let 142 - { sources, tracks } = 143 - model 144 - in 145 141 { model 146 142 | authentication = Authentication.Unauthenticated 147 143 , playlists = [] 148 144 , playlistToActivate = Nothing 149 145 150 - -- 146 + -- Queue 147 + -------- 151 148 , dontPlay = [] 152 149 , nowPlaying = Nothing 153 150 , playedPreviously = [] ··· 158 155 , repeat = False 159 156 , shuffle = False 160 157 161 - -- 158 + -- Sources 159 + ---------- 162 160 , processingContext = [] 163 161 , sources = [] 164 162 165 - -- 166 - , tracks = 167 - { tracks 168 - | collection = Tracks.emptyCollection 169 - , enabledSourceIds = [] 170 - , favourites = [] 171 - , hideDuplicates = Tracks.initialModel.hideDuplicates 172 - , nowPlaying = Nothing 173 - , searchResults = Nothing 174 - } 163 + -- Tracks 164 + --------- 165 + , favourites = [] 166 + , hideDuplicates = False 167 + , searchResults = Nothing 168 + , tracks = Tracks.emptyCollection 175 169 } 176 170 |> Backdrop.setDefault 177 171 |> Return.command (Ports.toBrain <| Alien.trigger Alien.SignOut) ··· 199 193 Return.singleton { model | contextMenu = Just (Authentication.moreOptionsMenu coordinates) } 200 194 201 195 Reply.ShowPlaylistListMenu coordinates playlist -> 202 - Return.singleton { model | contextMenu = Just (Playlists.listMenu playlist model.tracks.collection.identified model.confirmation coordinates) } 203 - 204 - ShowTracksContextMenu coordinates { alt } tracks -> 205 - let 206 - menuDependencies = 207 - { cached = model.tracks.cached 208 - , cachingInProgress = model.tracks.cachingInProgress 209 - , currentTime = model.currentTime 210 - , selectedPlaylist = model.tracks.selectedPlaylist 211 - , lastModifiedPlaylistName = model.lastModifiedPlaylist 212 - , showAlternativeMenu = alt 213 - , sources = model.sources 214 - } 215 - in 216 - Return.singleton { model | contextMenu = Just (Tracks.trackMenu menuDependencies tracks coordinates) } 217 - 218 - ShowTracksViewMenu coordinates maybeGrouping -> 219 - Return.singleton { model | contextMenu = Just (Tracks.viewMenu model.tracks.cachedOnly maybeGrouping coordinates) } 196 + Return.singleton { model | contextMenu = Just (Playlists.listMenu playlist model.tracks.identified model.confirmation coordinates) } 220 197 221 198 ----------------------------------------- 222 199 -- Last.fm ··· 281 258 ----------------------------------------- 282 259 Reply.ActivatePlaylist playlist -> 283 260 playlist 284 - |> Tracks.SelectPlaylist 285 - |> TracksMsg 261 + |> SelectPlaylist 286 262 |> Return.performanceF model 287 263 288 264 Reply.AddTracksToPlaylist a -> 289 265 Return.performance (UI.AddTracksToPlaylist a) model 290 266 291 267 Reply.DeactivatePlaylist -> 292 - Tracks.DeselectPlaylist 293 - |> TracksMsg 294 - |> Return.performanceF model 295 - 296 - GenerateDirectoryPlaylists -> 297 - let 298 - nonDirectoryPlaylists = 299 - List.filterNot 300 - .autoGenerated 301 - model.playlists 302 - 303 - directoryPlaylists = 304 - UI.Playlists.Directory.generate 305 - model.sources 306 - model.tracks.collection.untouched 307 - in 308 - [ nonDirectoryPlaylists 309 - , directoryPlaylists 310 - ] 311 - |> List.concat 312 - |> (\c -> { model | playlists = c }) 313 - |> Return.singleton 268 + Return.performanceF model DeselectPlaylist 314 269 315 270 RemoveFromSelectedPlaylist playlist tracks -> 316 271 let ··· 330 285 p 331 286 ) 332 287 |> (\c -> { model | playlists = c }) 333 - |> Return.performance (TracksMsg <| Tracks.SelectPlaylist updatedPlaylist) 288 + |> Return.performance (SelectPlaylist updatedPlaylist) 334 289 |> andThen (translate SavePlaylists) 335 290 336 291 RemovePlaylistFromCollection args -> ··· 338 293 |> DeletePlaylist 339 294 |> Return.performanceF { model | confirmation = Nothing } 340 295 341 - ReplacePlaylistInCollection playlist -> 342 - model.playlists 343 - |> List.map (\p -> ifThenElse (p.name == playlist.name) playlist p) 344 - |> (\c -> { model | playlists = c }) 345 - |> translate SavePlaylists 346 - 347 296 RequestAssistanceForPlaylists tracks -> 348 297 model.playlists 349 298 |> List.filterNot .autoGenerated ··· 371 320 MoveQueueItemToLast args -> 372 321 Queue.moveQueueItemToLast args model 373 322 374 - PlayTrack identifiedTrack -> 375 - identifiedTrack 376 - |> Queue.InjectFirstAndPlay 377 - |> QueueMsg 378 - |> Return.performanceF model 379 - 380 - ResetQueue -> 381 - Return.performance (QueueMsg Queue.Reset) model 382 - 383 323 RewindQueue -> 384 324 Return.performance (QueueMsg Queue.Rewind) model 385 325 ··· 402 342 |> Return.performanceF model 403 343 404 344 ClearTracksCache -> 405 - model.tracks.cached 345 + model.cachedTracks 406 346 |> Json.Encode.list Json.Encode.string 407 347 |> Alien.broadcast Alien.RemoveTracksFromCache 408 348 |> Ports.toBrain 409 - |> return (Lens.modify Tracks.lens (\m -> { m | cached = [] }) model) 349 + |> return { model | cachedTracks = [] } 410 350 |> andThen (Return.performance <| TracksMsg Tracks.Harvest) 411 351 |> andThen (translate <| Reply.SaveEnclosedUserData) 412 352 |> andThen (translate <| ShowWarningNotification "Tracks cache was cleared") ··· 475 415 |> Json.Encode.list Json.Encode.string 476 416 |> Alien.broadcast Alien.RemoveTracksFromCache 477 417 |> Ports.toBrain 478 - |> return 479 - (Lens.modify Tracks.lens 480 - (\m -> { m | cached = List.without trackIds m.cached }) 481 - model 482 - ) 418 + |> return { model | cachedTracks = List.without trackIds model.cachedTracks } 483 419 |> andThen (Return.performance <| TracksMsg Tracks.Harvest) 484 420 |> andThen (translate Reply.SaveEnclosedUserData) 485 421 ··· 536 472 ) 537 473 |> Alien.broadcast Alien.StoreTracksInCache 538 474 |> Ports.toBrain 539 - |> return 540 - (Lens.modify Tracks.lens 541 - (\m -> { m | cachingInProgress = m.cachingInProgress ++ trackIds }) 542 - model 543 - ) 475 + |> return { model | cachingTracksInProgress = model.cachingTracksInProgress ++ trackIds } 544 476 |> andThen (showNotification notification) 545 477 546 478 ToggleCachedTracksOnly -> ··· 564 496 |> Return.performanceF model 565 497 566 498 Export -> 567 - { favourites = model.tracks.favourites 499 + { favourites = model.favourites 568 500 , playlists = List.filterNot .autoGenerated model.playlists 569 501 , progress = model.progress 570 - , settings = Just (gatherSettings model) 502 + , settings = Just (Settings.gatherSettings model) 571 503 , sources = model.sources 572 - , tracks = model.tracks.collection.untouched 504 + , tracks = model.tracks.untouched 573 505 } 574 506 |> encodeHypaethralData 575 507 |> Json.Encode.encode 2 ··· 595 527 Return.performance UI.SaveEnclosedUserData model 596 528 597 529 SaveFavourites -> 598 - model.tracks.favourites 530 + model.favourites 599 531 |> Json.Encode.list Tracks.encodeFavourite 600 532 |> Alien.broadcast Alien.SaveFavourites 601 533 |> Ports.toBrain ··· 617 549 |> return model 618 550 619 551 SaveSettings -> 620 - model 621 - |> gatherSettings 622 - |> Settings.encode 623 - |> Alien.broadcast Alien.SaveSettings 624 - |> Ports.toBrain 625 - |> return model 552 + Settings.save model 626 553 627 554 SaveSources -> 628 - let 629 - updateEnabledSourceIdsOnTracks = 630 - model.sources 631 - |> Sources.enabledSourceIds 632 - |> Tracks.SetEnabledSourceIds 633 - |> TracksMsg 634 - |> Return.performance 635 - 636 - ( updatedModel, updatedCmd ) = 637 - updateEnabledSourceIdsOnTracks model 638 - in 639 - updatedModel.sources 555 + model.sources 640 556 |> Json.Encode.list Sources.encode 641 557 |> Alien.broadcast Alien.SaveSources 642 558 |> Ports.toBrain 643 - |> Return.return updatedModel 644 - |> Return.command updatedCmd 559 + |> Return.return model 645 560 646 561 SaveTracks -> 647 - model.tracks.collection.untouched 562 + model.tracks.untouched 648 563 |> Json.Encode.list Tracks.encodeTrack 649 564 |> Alien.broadcast Alien.SaveTracks 650 565 |> Ports.toBrain ··· 681 596 ) 682 597 return 683 598 hypaethralBit.list 684 - 685 - 686 - 687 - -- USER 688 - 689 - 690 - gatherSettings : Model -> Settings.Settings 691 - gatherSettings { chosenBackdrop, lastFm, processAutomatically, rememberProgress, tracks } = 692 - { backgroundImage = chosenBackdrop 693 - , hideDuplicates = tracks.hideDuplicates 694 - , lastFm = lastFm.sessionKey 695 - , processAutomatically = processAutomatically 696 - , rememberProgress = rememberProgress 697 - }
+1 -1
src/Applications/UI/Services/State.elm
··· 32 32 33 33 scrobble : { duration : Int, timestamp : Int, trackId : String } -> Manager 34 34 scrobble { duration, timestamp, trackId } model = 35 - case model.tracks.nowPlaying of 35 + case Maybe.map .identifiedTrack model.nowPlaying of 36 36 Just ( _, track ) -> 37 37 if trackId == track.id then 38 38 ( model
+32
src/Applications/UI/Settings/State.elm
··· 1 + module UI.Settings.State exposing (..) 2 + 3 + import Alien 4 + import Return exposing (return) 5 + import Settings exposing (Settings) 6 + import UI.Ports as Ports 7 + import UI.Types exposing (..) 8 + 9 + 10 + 11 + -- 🔱 12 + -- TODO: Move to User.State.Export 13 + 14 + 15 + gatherSettings : Model -> Settings 16 + gatherSettings { chosenBackdrop, hideDuplicates, lastFm, processAutomatically, rememberProgress } = 17 + { backgroundImage = chosenBackdrop 18 + , hideDuplicates = hideDuplicates 19 + , lastFm = lastFm.sessionKey 20 + , processAutomatically = processAutomatically 21 + , rememberProgress = rememberProgress 22 + } 23 + 24 + 25 + save : Manager 26 + save model = 27 + model 28 + |> gatherSettings 29 + |> Settings.encode 30 + |> Alien.broadcast Alien.SaveSettings 31 + |> Ports.toBrain 32 + |> return model
+2 -2
src/Applications/UI/Sources/State.elm
··· 138 138 139 139 140 140 141 - -- 🛠 141 + -- 🔱 142 142 143 143 144 144 finishedProcessing : Manager ··· 479 479 ) 480 480 |> (\collection -> { model | sources = collection }) 481 481 |> Return.performance (UI.Reply Reply.SaveSources) 482 - |> andThen (Return.performance <| UI.Reply Reply.GenerateDirectoryPlaylists) 482 + |> andThen Common.generateDirectoryPlaylists 483 483 484 484 485 485
+1 -1
src/Applications/UI/Sources/View.elm
··· 45 45 Index -> 46 46 Lazy.lazy2 47 47 (\a b -> receptacle <| index a b) 48 - (List.length model.tracks.collection.untouched) 48 + (List.length model.tracks.untouched) 49 49 { processingContext = model.processingContext 50 50 , processingError = model.processingError 51 51 , sources = model.sources
-996
src/Applications/UI/Tracks.elm
··· 1 - module UI.Tracks exposing (Dependencies, Model, Msg(..), Scene(..), importHypaethral, initialModel, update, view) 2 - 3 - import Alien 4 - import Chunky exposing (..) 5 - import Color exposing (Color) 6 - import Color.Ext as Color 7 - import Common exposing (Switch(..)) 8 - import Conditional exposing (ifThenElse) 9 - import Coordinates exposing (Coordinates, Viewport) 10 - import Css.Classes as C 11 - import Html exposing (Html, text) 12 - import Html.Attributes exposing (href, placeholder, style, tabindex, target, title, value) 13 - import Html.Events exposing (onBlur, onClick, onInput) 14 - import Html.Events.Extra.Mouse as Mouse 15 - import Html.Ext exposing (onEnterKey) 16 - import Html.Lazy exposing (..) 17 - import InfiniteList 18 - import Json.Decode as Json 19 - import Json.Encode 20 - import List.Ext as List 21 - import List.Extra as List 22 - import Material.Icons as Icons 23 - import Material.Icons.Types exposing (Coloring(..)) 24 - import Maybe.Extra as Maybe 25 - import Playlists exposing (Playlist) 26 - import Return3 as Return exposing (..) 27 - import Sources 28 - import Task.Extra as Task 29 - import Tracks exposing (..) 30 - import Tracks.Collection as Collection exposing (..) 31 - import Tracks.Encoding as Encoding 32 - import Tracks.Favourites as Favourites 33 - import UI.DnD as DnD 34 - import UI.Kit 35 - import UI.Navigation exposing (..) 36 - import UI.Page 37 - import UI.Playlists.Page 38 - import UI.Ports 39 - import UI.Queue.Page 40 - import UI.Reply as UI exposing (Reply(..)) 41 - import UI.Sources.Page as Sources 42 - import UI.Tracks.Reply as Tracks exposing (Reply(..)) 43 - import UI.Tracks.Scene.List 44 - import User.Layer exposing (HypaethralData) 45 - 46 - 47 - 48 - -- 🌳 49 - 50 - 51 - type alias Model = 52 - { cached : List String 53 - , cachedOnly : Bool 54 - , cachingInProgress : List String 55 - , collection : Collection 56 - , enabledSourceIds : List String 57 - , favourites : List Favourite 58 - , favouritesOnly : Bool 59 - , grouping : Maybe Grouping 60 - , hideDuplicates : Bool 61 - , nowPlaying : Maybe IdentifiedTrack 62 - , scene : Scene 63 - , searchResults : Maybe (List String) 64 - , searchTerm : Maybe String 65 - , selectedPlaylist : Maybe Playlist 66 - , selectedTrackIndexes : List Int 67 - , sortBy : SortBy 68 - , sortDirection : SortDirection 69 - 70 - ----------------------------------------- 71 - -- Scenes 72 - ----------------------------------------- 73 - , listScene : UI.Tracks.Scene.List.Model 74 - } 75 - 76 - 77 - type Scene 78 - = List 79 - 80 - 81 - initialModel : Model 82 - initialModel = 83 - { cached = [] 84 - , cachedOnly = False 85 - , cachingInProgress = [] 86 - , collection = emptyCollection 87 - , enabledSourceIds = [] 88 - , favourites = [] 89 - , favouritesOnly = False 90 - , grouping = Nothing 91 - , hideDuplicates = False 92 - , nowPlaying = Nothing 93 - , scene = List 94 - , searchResults = Nothing 95 - , searchTerm = Nothing 96 - , selectedPlaylist = Nothing 97 - , selectedTrackIndexes = [] 98 - , sortBy = Artist 99 - , sortDirection = Asc 100 - 101 - ----------------------------------------- 102 - -- Scenes 103 - ----------------------------------------- 104 - , listScene = UI.Tracks.Scene.List.initialModel 105 - } 106 - 107 - 108 - 109 - -- 📣 110 - 111 - 112 - type Msg 113 - = Bypass 114 - | Harvest 115 - | Reply UI.Reply 116 - | ScrollToNowPlaying 117 - | SetEnabledSourceIds (List String) 118 - | SetNowPlaying (Maybe IdentifiedTrack) 119 - | ToggleCachedOnly 120 - | ToggleFavouritesOnly 121 - | ToggleHideDuplicates 122 - ----------------------------------------- 123 - -- Collection 124 - ----------------------------------------- 125 - | Add Json.Value 126 - | RemoveByPaths Json.Value 127 - | RemoveBySourceId String 128 - ----------------------------------------- 129 - -- Groups 130 - ----------------------------------------- 131 - | DisableGrouping 132 - | GroupBy Grouping 133 - ----------------------------------------- 134 - -- Menus 135 - ----------------------------------------- 136 - | ShowTrackMenu Int { alt : Bool } Coordinates 137 - | ShowViewMenu (Maybe Grouping) Mouse.Event 138 - ----------------------------------------- 139 - -- Playlists 140 - ----------------------------------------- 141 - | DeselectPlaylist 142 - | SelectPlaylist Playlist 143 - ----------------------------------------- 144 - -- Scenes 145 - ----------------------------------------- 146 - | ListSceneMsg UI.Tracks.Scene.List.Msg 147 - ----------------------------------------- 148 - -- Search 149 - ----------------------------------------- 150 - | ClearSearch 151 - | Search 152 - | SetSearchResults Json.Value 153 - | SetSearchTerm String 154 - 155 - 156 - update : Msg -> Model -> Return Model Msg UI.Reply 157 - update msg model = 158 - case msg of 159 - Bypass -> 160 - return model 161 - 162 - Harvest -> 163 - reviseCollection harvest model 164 - 165 - Reply reply -> 166 - returnReplyWithModel model reply 167 - 168 - ScrollToNowPlaying -> 169 - model.nowPlaying 170 - |> Maybe.map (Tuple.second >> .id) 171 - |> Maybe.andThen 172 - (\id -> 173 - List.find 174 - (Tuple.second >> .id >> (==) id) 175 - model.collection.harvested 176 - ) 177 - |> Maybe.map 178 - (case model.scene of 179 - List -> 180 - UI.Tracks.Scene.List.scrollToNowPlaying model.collection.harvested 181 - >> Cmd.map ListSceneMsg 182 - ) 183 - |> Maybe.map 184 - (\cmd -> 185 - cmd 186 - |> Return.commandWithModel model 187 - |> Return.addReply (GoToPage UI.Page.Index) 188 - ) 189 - |> Maybe.withDefault (return model) 190 - 191 - SetEnabledSourceIds sourceIds -> 192 - reviseCollection identify 193 - { model | enabledSourceIds = sourceIds } 194 - 195 - SetNowPlaying maybeIdentifiedTrack -> 196 - return { model | nowPlaying = maybeIdentifiedTrack } 197 - 198 - ToggleCachedOnly -> 199 - { model | cachedOnly = not model.cachedOnly } 200 - |> reviseCollection harvest 201 - |> addReply SaveEnclosedUserData 202 - 203 - ToggleFavouritesOnly -> 204 - { model | favouritesOnly = not model.favouritesOnly } 205 - |> reviseCollection harvest 206 - |> addReply SaveEnclosedUserData 207 - 208 - ToggleHideDuplicates -> 209 - { model | hideDuplicates = not model.hideDuplicates } 210 - |> reviseCollection arrange 211 - |> addReply SaveSettings 212 - 213 - ----------------------------------------- 214 - -- Collection 215 - ----------------------------------------- 216 - -- # Add 217 - -- > Add tracks to the collection. 218 - -- 219 - Add json -> 220 - reviseCollection 221 - (json 222 - |> Json.decodeValue (Json.list Encoding.trackDecoder) 223 - |> Result.withDefault [] 224 - |> add 225 - ) 226 - model 227 - 228 - -- # Remove 229 - -- > Remove tracks from the collection. 230 - -- 231 - RemoveByPaths json -> 232 - let 233 - decoder = 234 - Json.map2 235 - Tuple.pair 236 - (Json.field "filePaths" <| Json.list Json.string) 237 - (Json.field "sourceId" Json.string) 238 - 239 - ( paths, sourceId ) = 240 - json 241 - |> Json.decodeValue decoder 242 - |> Result.withDefault ( [], missingId ) 243 - 244 - { kept, removed } = 245 - Tracks.removeByPaths 246 - { sourceId = sourceId, paths = paths } 247 - model.collection.untouched 248 - 249 - newCollection = 250 - { emptyCollection | untouched = kept } 251 - in 252 - { model | collection = newCollection } 253 - |> reviseCollection identify 254 - |> addReply (RemoveTracksFromCache removed) 255 - 256 - RemoveBySourceId sourceId -> 257 - let 258 - { kept, removed } = 259 - Tracks.removeBySourceId sourceId model.collection.untouched 260 - 261 - newCollection = 262 - { emptyCollection | untouched = kept } 263 - in 264 - { model | collection = newCollection } 265 - |> reviseCollection identify 266 - |> addReply (RemoveTracksFromCache removed) 267 - 268 - ----------------------------------------- 269 - -- Groups 270 - ----------------------------------------- 271 - DisableGrouping -> 272 - { model | grouping = Nothing } 273 - |> reviseCollection arrange 274 - |> addReply SaveEnclosedUserData 275 - 276 - GroupBy grouping -> 277 - { model | grouping = Just grouping } 278 - |> reviseCollection arrange 279 - |> addReply SaveEnclosedUserData 280 - 281 - ----------------------------------------- 282 - -- Menus 283 - ----------------------------------------- 284 - ShowTrackMenu trackIndex options coordinates -> 285 - let 286 - listScene = 287 - model.listScene 288 - 289 - selection = 290 - if List.isEmpty model.selectedTrackIndexes then 291 - [ trackIndex ] 292 - 293 - else if List.member trackIndex model.selectedTrackIndexes == False then 294 - [ trackIndex ] 295 - 296 - else 297 - model.selectedTrackIndexes 298 - in 299 - model.collection.harvested 300 - |> List.pickIndexes selection 301 - |> ShowTracksContextMenu coordinates options 302 - |> returnReplyWithModel 303 - { model 304 - | listScene = { listScene | dnd = DnD.initialModel } 305 - , selectedTrackIndexes = selection 306 - } 307 - 308 - ShowViewMenu grouping mouseEvent -> 309 - grouping 310 - |> ShowTracksViewMenu (Coordinates.fromTuple mouseEvent.clientPos) 311 - |> returnReplyWithModel model 312 - 313 - ----------------------------------------- 314 - -- Playlists 315 - ----------------------------------------- 316 - DeselectPlaylist -> 317 - { model | selectedPlaylist = Nothing } 318 - |> reviseCollection arrange 319 - |> addReply SaveEnclosedUserData 320 - 321 - SelectPlaylist playlist -> 322 - { model | selectedPlaylist = Just playlist } 323 - |> reviseCollection arrange 324 - |> addReply SaveEnclosedUserData 325 - 326 - ----------------------------------------- 327 - -- Scenes 328 - ----------------------------------------- 329 - ListSceneMsg sub -> 330 - Return.castNested 331 - translateReply 332 - { mapCmd = ListSceneMsg 333 - , mapModel = \child -> { model | listScene = child } 334 - , update = UI.Tracks.Scene.List.update 335 - } 336 - { model = model.listScene 337 - , msg = sub 338 - } 339 - 340 - ----------------------------------------- 341 - -- Search 342 - ----------------------------------------- 343 - ClearSearch -> 344 - { model | searchResults = Nothing, searchTerm = Nothing } 345 - |> reviseCollection harvest 346 - |> addReply SaveEnclosedUserData 347 - 348 - Search -> 349 - case ( model.searchTerm, model.searchResults ) of 350 - ( Just term, _ ) -> 351 - term 352 - |> String.trim 353 - |> Json.Encode.string 354 - |> UI.Ports.giveBrain Alien.SearchTracks 355 - |> Return.commandWithModel model 356 - 357 - ( Nothing, Just _ ) -> 358 - reviseCollection harvest { model | searchResults = Nothing } 359 - 360 - ( Nothing, Nothing ) -> 361 - return model 362 - 363 - SetSearchResults json -> 364 - case model.searchTerm of 365 - Just _ -> 366 - json 367 - |> Json.decodeValue (Json.list Json.string) 368 - |> Result.withDefault [] 369 - |> (\results -> { model | searchResults = Just results }) 370 - |> reviseCollection harvest 371 - |> addReply (ToggleLoadingScreen Off) 372 - 373 - Nothing -> 374 - return model 375 - 376 - SetSearchTerm term -> 377 - addReplies 378 - [ SaveEnclosedUserData ] 379 - (case String.trim term of 380 - "" -> 381 - return { model | searchTerm = Nothing } 382 - 383 - _ -> 384 - return { model | searchTerm = Just term } 385 - ) 386 - 387 - 388 - importHypaethral : Model -> HypaethralData -> Maybe Playlist -> Return Model Msg UI.Reply 389 - importHypaethral model data selectedPlaylist = 390 - let 391 - adjustedModel = 392 - { model 393 - | collection = { emptyCollection | untouched = data.tracks } 394 - , enabledSourceIds = Sources.enabledSourceIds data.sources 395 - , favourites = data.favourites 396 - , hideDuplicates = Maybe.unwrap False .hideDuplicates data.settings 397 - , selectedPlaylist = selectedPlaylist 398 - } 399 - 400 - addReplyIfNecessary = 401 - case model.searchTerm of 402 - Just _ -> 403 - identity 404 - 405 - Nothing -> 406 - addReply (ToggleLoadingScreen Off) 407 - in 408 - adjustedModel 409 - |> makeParcel 410 - |> identify 411 - |> resolveParcel adjustedModel 412 - |> andThen (update Search) 413 - |> addReplyIfNecessary 414 - 415 - 416 - 417 - -- 📣 ░░ CHILDREN & REPLIES 418 - 419 - 420 - translateReply : Tracks.Reply -> Model -> Return Model Msg UI.Reply 421 - translateReply reply model = 422 - case reply of 423 - Transcend uiReplies -> 424 - returnRepliesWithModel model uiReplies 425 - 426 - -- 427 - MarkAsSelected indexInList { shiftKey } -> 428 - let 429 - selection = 430 - if shiftKey then 431 - model.selectedTrackIndexes 432 - |> List.head 433 - |> Maybe.map 434 - (\n -> 435 - if n > indexInList then 436 - List.range indexInList n 437 - 438 - else 439 - List.range n indexInList 440 - ) 441 - |> Maybe.withDefault [ indexInList ] 442 - 443 - else 444 - [ indexInList ] 445 - in 446 - return { model | selectedTrackIndexes = selection } 447 - 448 - MoveTrackInSelectedPlaylist { to } -> 449 - case model.selectedPlaylist of 450 - Just p -> 451 - let 452 - moveParams = 453 - { from = Maybe.withDefault 0 (List.head model.selectedTrackIndexes) 454 - , to = to 455 - , amount = List.length model.selectedTrackIndexes 456 - } 457 - 458 - updatedPlaylist = 459 - { p | tracks = List.move moveParams p.tracks } 460 - in 461 - { model | selectedPlaylist = Just updatedPlaylist } 462 - |> reviseCollection arrange 463 - |> addReply (ReplacePlaylistInCollection updatedPlaylist) 464 - 465 - Nothing -> 466 - return model 467 - 468 - ShowTrackMenuWithoutDelay a b c -> 469 - update (ShowTrackMenu a b c) model 470 - 471 - ShowTrackMenuWithSmallDelay a b c -> 472 - ShowTrackMenu a b c 473 - |> Task.doDelayed 250 474 - |> returnCommandWithModel model 475 - 476 - SortBy property -> 477 - let 478 - sortDir = 479 - if model.sortBy /= property then 480 - Asc 481 - 482 - else if model.sortDirection == Asc then 483 - Desc 484 - 485 - else 486 - Asc 487 - in 488 - { model | sortBy = property, sortDirection = sortDir } 489 - |> reviseCollection arrange 490 - |> addReply SaveEnclosedUserData 491 - 492 - ToggleFavourite index -> 493 - model.collection.harvested 494 - |> List.getAt index 495 - |> Maybe.map (toggleFavourite model) 496 - |> Maybe.withDefault (return model) 497 - 498 - 499 - 500 - -- 📣 ░░ PARCEL 501 - 502 - 503 - makeParcel : Model -> Parcel 504 - makeParcel model = 505 - ( { cached = model.cached 506 - , cachedOnly = model.cachedOnly 507 - , enabledSourceIds = model.enabledSourceIds 508 - , favourites = model.favourites 509 - , favouritesOnly = model.favouritesOnly 510 - , grouping = model.grouping 511 - , hideDuplicates = model.hideDuplicates 512 - , searchResults = model.searchResults 513 - , selectedPlaylist = model.selectedPlaylist 514 - , sortBy = model.sortBy 515 - , sortDirection = model.sortDirection 516 - } 517 - , model.collection 518 - ) 519 - 520 - 521 - resolveParcel : Model -> Parcel -> Return Model Msg UI.Reply 522 - resolveParcel model ( deps, newCollection ) = 523 - let 524 - scrollObj = 525 - Json.Encode.object 526 - [ ( "scrollTop", Json.Encode.int 0 ) ] 527 - 528 - scrollEvent = 529 - Json.Encode.object 530 - [ ( "target", scrollObj ) ] 531 - 532 - newScrollContext = 533 - scrollContext model 534 - 535 - collectionChanged = 536 - Collection.tracksChanged 537 - model.collection.untouched 538 - newCollection.untouched 539 - 540 - harvestChanged = 541 - Collection.harvestChanged 542 - model.collection.harvested 543 - newCollection.harvested 544 - 545 - searchChanged = 546 - newScrollContext /= model.collection.scrollContext 547 - 548 - listSceneModel = 549 - model.listScene 550 - 551 - listScene = 552 - if model.scene == List && searchChanged then 553 - { listSceneModel | infiniteList = InfiniteList.updateScroll scrollEvent listSceneModel.infiniteList } 554 - 555 - else 556 - listSceneModel 557 - 558 - modelWithNewCollection = 559 - { model 560 - | collection = { newCollection | scrollContext = newScrollContext } 561 - , listScene = listScene 562 - , selectedTrackIndexes = 563 - if collectionChanged || harvestChanged then 564 - [] 565 - 566 - else 567 - model.selectedTrackIndexes 568 - } 569 - in 570 - ( modelWithNewCollection 571 - ---------- 572 - -- Command 573 - ---------- 574 - , if searchChanged then 575 - case model.scene of 576 - List -> 577 - Cmd.map ListSceneMsg UI.Tracks.Scene.List.scrollToTop 578 - 579 - else 580 - Cmd.none 581 - -------- 582 - -- Reply 583 - -------- 584 - , if collectionChanged then 585 - [ GenerateDirectoryPlaylists, ResetQueue ] 586 - 587 - else if harvestChanged then 588 - [ ResetQueue ] 589 - 590 - else 591 - [] 592 - ) 593 - 594 - 595 - reviseCollection : (Parcel -> Parcel) -> Model -> Return Model Msg UI.Reply 596 - reviseCollection collector model = 597 - model 598 - |> makeParcel 599 - |> collector 600 - |> resolveParcel model 601 - 602 - 603 - scrollContext : Model -> String 604 - scrollContext model = 605 - String.concat 606 - [ Maybe.withDefault "" <| model.searchTerm 607 - , Maybe.withDefault "" <| Maybe.map .name model.selectedPlaylist 608 - ] 609 - 610 - 611 - 612 - -- 📣 ░░ FAVOURITES 613 - 614 - 615 - toggleFavourite : Model -> IdentifiedTrack -> Return Model Msg UI.Reply 616 - toggleFavourite model ( i, t ) = 617 - let 618 - newFavourites = 619 - Favourites.toggleInFavouritesList ( i, t ) model.favourites 620 - 621 - effect = 622 - if model.favouritesOnly then 623 - Collection.map (Favourites.toggleInTracksList t) >> harvest 624 - 625 - else 626 - Collection.map (Favourites.toggleInTracksList t) 627 - in 628 - { model | favourites = newFavourites } 629 - |> reviseCollection effect 630 - |> addReply SaveFavourites 631 - 632 - 633 - 634 - -- 🗺 635 - 636 - 637 - type alias Dependencies = 638 - { amountOfSources : Int 639 - , bgColor : Maybe Color 640 - , darkMode : Bool 641 - , isOnIndexPage : Bool 642 - , isTouchDevice : Bool 643 - , sourceIdsBeingProcessed : List String 644 - , viewport : Viewport 645 - } 646 - 647 - 648 - view : Model -> Dependencies -> Html Msg 649 - view model deps = 650 - chunk 651 - viewClasses 652 - [ lazy6 653 - navigation 654 - model.grouping 655 - model.favouritesOnly 656 - model.searchTerm 657 - model.selectedPlaylist 658 - deps.isOnIndexPage 659 - deps.bgColor 660 - 661 - -- 662 - , if List.isEmpty model.collection.harvested then 663 - lazy4 664 - noTracksView 665 - deps.sourceIdsBeingProcessed 666 - deps.amountOfSources 667 - (List.length model.collection.harvested) 668 - (List.length model.favourites) 669 - 670 - else 671 - case model.scene of 672 - List -> 673 - listView model deps 674 - ] 675 - 676 - 677 - viewClasses : List String 678 - viewClasses = 679 - [ C.flex 680 - , C.flex_col 681 - , C.flex_grow 682 - ] 683 - 684 - 685 - navigation : Maybe Grouping -> Bool -> Maybe String -> Maybe Playlist -> Bool -> Maybe Color -> Html Msg 686 - navigation maybeGrouping favouritesOnly searchTerm selectedPlaylist isOnIndexPage bgColor = 687 - let 688 - tabindex_ = 689 - ifThenElse isOnIndexPage 0 -1 690 - in 691 - chunk 692 - [ C.flex ] 693 - [ ----------------------------------------- 694 - -- Part 1 695 - ----------------------------------------- 696 - chunk 697 - [ C.border_b 698 - , C.border_r 699 - , C.border_gray_300 700 - , C.flex 701 - , C.flex_grow 702 - , C.mt_px 703 - , C.overflow_hidden 704 - , C.relative 705 - , C.text_gray_600 706 - 707 - -- Dark mode 708 - ------------ 709 - , C.dark__border_base01 710 - , C.dark__text_base04 711 - ] 712 - [ -- Input 713 - -------- 714 - slab 715 - Html.input 716 - [ onBlur Search 717 - , onEnterKey Search 718 - , onInput SetSearchTerm 719 - , placeholder "Search" 720 - , tabindex tabindex_ 721 - , value (Maybe.withDefault "" searchTerm) 722 - ] 723 - [ C.bg_transparent 724 - , C.border_none 725 - , C.flex_grow 726 - , C.h_full 727 - , C.ml_1 728 - , C.mt_px 729 - , C.outline_none 730 - , C.pl_8 731 - , C.pr_2 732 - , C.pt_px 733 - , C.text_base02 734 - , C.text_sm 735 - , C.w_full 736 - 737 - -- Dark mode 738 - ------------ 739 - , C.dark__text_base06 740 - ] 741 - [] 742 - 743 - -- Search icon 744 - -------------- 745 - , chunk 746 - [ C.absolute 747 - , C.bottom_0 748 - , C.flex 749 - , C.items_center 750 - , C.left_0 751 - , C.ml_3 752 - , C.mt_px 753 - , C.top_0 754 - , C.z_0 755 - ] 756 - [ Icons.search 16 Inherit ] 757 - 758 - -- Actions 759 - ---------- 760 - , chunk 761 - [ C.flex 762 - , C.items_center 763 - , C.mr_3 764 - , C.mt_px 765 - , C.pt_px 766 - ] 767 - [ -- 1 768 - case searchTerm of 769 - Just _ -> 770 - brick 771 - [ onClick ClearSearch 772 - , title "Clear search" 773 - ] 774 - [ C.cursor_pointer 775 - , C.ml_1 776 - , C.mt_px 777 - ] 778 - [ Icons.clear 16 Inherit ] 779 - 780 - Nothing -> 781 - nothing 782 - 783 - -- 2 784 - , brick 785 - [ onClick ToggleFavouritesOnly 786 - , title "Toggle favourites-only" 787 - ] 788 - [ C.cursor_pointer 789 - , C.ml_1 790 - ] 791 - [ case favouritesOnly of 792 - True -> 793 - Icons.favorite 16 (Color UI.Kit.colorKit.base08) 794 - 795 - False -> 796 - Icons.favorite_border 16 Inherit 797 - ] 798 - 799 - -- 3 800 - , brick 801 - [ Mouse.onClick (ShowViewMenu maybeGrouping) 802 - , title "View settings" 803 - ] 804 - [ C.cursor_pointer 805 - , C.ml_1 806 - ] 807 - [ Icons.more_vert 16 Inherit ] 808 - 809 - -- 4 810 - , case selectedPlaylist of 811 - Just playlist -> 812 - brick 813 - [ onClick DeselectPlaylist 814 - 815 - -- 816 - , bgColor 817 - |> Maybe.withDefault UI.Kit.colorKit.base01 818 - |> Color.toCssString 819 - |> style "background-color" 820 - ] 821 - [ C.antialiased 822 - , C.cursor_pointer 823 - , C.duration_500 824 - , C.font_bold 825 - , C.leading_none 826 - , C.ml_1 827 - , C.px_1 828 - , C.py_1 829 - , C.rounded 830 - , C.truncate 831 - , C.text_white_90 832 - , C.text_xxs 833 - , C.transition 834 - 835 - -- Dark mode 836 - ------------ 837 - , C.dark__text_white_60 838 - ] 839 - [ chunk 840 - [ C.px_px, C.pt_px ] 841 - [ text playlist.name ] 842 - ] 843 - 844 - Nothing -> 845 - nothing 846 - ] 847 - ] 848 - , ----------------------------------------- 849 - -- Part 2 850 - ----------------------------------------- 851 - UI.Navigation.localWithTabindex 852 - tabindex_ 853 - [ ( Icon Icons.waves 854 - , Label "Playlists" Hidden 855 - , NavigateToPage (UI.Page.Playlists UI.Playlists.Page.Index) 856 - ) 857 - , ( Icon Icons.schedule 858 - , Label "Queue" Hidden 859 - , NavigateToPage (UI.Page.Queue UI.Queue.Page.Index) 860 - ) 861 - , ( Icon Icons.equalizer 862 - , Label "Equalizer" Hidden 863 - , NavigateToPage UI.Page.Equalizer 864 - ) 865 - ] 866 - ] 867 - 868 - 869 - noTracksView : List String -> Int -> Int -> Int -> Html Msg 870 - noTracksView processingContext amountOfSources amountOfTracks amountOfFavourites = 871 - chunk 872 - [ C.flex, C.flex_grow ] 873 - [ UI.Kit.centeredContent 874 - [ if List.length processingContext > 0 then 875 - message "Processing Tracks" 876 - 877 - else if amountOfSources == 0 then 878 - chunk 879 - [ C.flex 880 - , C.flex_wrap 881 - , C.items_start 882 - , C.justify_center 883 - , C.px_3 884 - ] 885 - [ -- Add 886 - ------ 887 - inline 888 - [ C.mb_3, C.mx_2, C.whitespace_no_wrap ] 889 - [ UI.Kit.buttonLink 890 - (Sources.NewOnboarding 891 - |> UI.Page.Sources 892 - |> UI.Page.toString 893 - ) 894 - UI.Kit.Filled 895 - (buttonContents 896 - [ UI.Kit.inlineIcon Icons.add 897 - , text "Add some music" 898 - ] 899 - ) 900 - ] 901 - 902 - -- Demo 903 - ------- 904 - , inline 905 - [ C.mb_3, C.mx_2, C.whitespace_no_wrap ] 906 - [ UI.Kit.buttonWithColor 907 - UI.Kit.Gray 908 - UI.Kit.Normal 909 - (Reply InsertDemo) 910 - (buttonContents 911 - [ UI.Kit.inlineIcon Icons.music_note 912 - , text "Insert demo" 913 - ] 914 - ) 915 - ] 916 - 917 - -- How 918 - ------ 919 - , inline 920 - [ C.mb_3, C.mx_2, C.whitespace_no_wrap ] 921 - [ UI.Kit.buttonWithOptions 922 - Html.a 923 - [ href "about" 924 - , target "_blank" 925 - ] 926 - UI.Kit.Gray 927 - UI.Kit.Normal 928 - Nothing 929 - (buttonContents 930 - [ UI.Kit.inlineIcon Icons.help 931 - , text "More info" 932 - ] 933 - ) 934 - ] 935 - ] 936 - 937 - else if amountOfTracks == 0 then 938 - message "No tracks found" 939 - 940 - else 941 - message "No sources available" 942 - ] 943 - ] 944 - 945 - 946 - buttonContents : List (Html Msg) -> Html Msg 947 - buttonContents = 948 - inline 949 - [ C.flex 950 - , C.items_center 951 - , C.leading_0 952 - ] 953 - 954 - 955 - message : String -> Html Msg 956 - message m = 957 - chunk 958 - [ C.border_b_2 959 - , C.border_current_color 960 - , C.text_sm 961 - , C.font_semibold 962 - , C.leading_snug 963 - , C.pb_1 964 - ] 965 - [ text m ] 966 - 967 - 968 - listView : Model -> Dependencies -> Html Msg 969 - listView model deps = 970 - model.selectedPlaylist 971 - |> Maybe.map .autoGenerated 972 - |> Maybe.andThen 973 - (\bool -> 974 - if bool then 975 - Nothing 976 - 977 - else 978 - Just model.listScene.dnd 979 - ) 980 - |> UI.Tracks.Scene.List.view 981 - { bgColor = deps.bgColor 982 - , darkMode = deps.darkMode 983 - , height = deps.viewport.height 984 - , isTouchDevice = deps.isTouchDevice 985 - , isVisible = deps.isOnIndexPage 986 - , showAlbum = deps.viewport.width >= 720 987 - } 988 - model.collection.harvested 989 - model.listScene.infiniteList 990 - model.favouritesOnly 991 - model.nowPlaying 992 - model.searchTerm 993 - model.sortBy 994 - model.sortDirection 995 - model.selectedTrackIndexes 996 - |> Html.map ListSceneMsg
-1
src/Applications/UI/Tracks/ContextMenu.elm
··· 11 11 import Time 12 12 import Tracks exposing (Grouping(..), IdentifiedTrack) 13 13 import UI.Reply exposing (Reply(..)) 14 - import UI.Tracks as Tracks 15 14 16 15 17 16
-20
src/Applications/UI/Tracks/Reply.elm
··· 1 - module UI.Tracks.Reply exposing (Reply(..)) 2 - 3 - import Coordinates exposing (Coordinates) 4 - import Tracks exposing (..) 5 - import UI.Reply as UI 6 - 7 - 8 - 9 - -- 🌳 10 - 11 - 12 - type Reply 13 - = Transcend (List UI.Reply) 14 - -- 15 - | MarkAsSelected Int { shiftKey : Bool } 16 - | MoveTrackInSelectedPlaylist { to : Int } 17 - | ShowTrackMenuWithoutDelay Int { alt : Bool } Coordinates 18 - | ShowTrackMenuWithSmallDelay Int { alt : Bool } Coordinates 19 - | SortBy SortBy 20 - | ToggleFavourite Int
+39 -89
src/Applications/UI/Tracks/Scene/List.elm
··· 1 - module UI.Tracks.Scene.List exposing (Model, Msg(..), containerId, initialModel, scrollToNowPlaying, scrollToTop, update, view) 1 + module UI.Tracks.Scene.List exposing (Dependencies, DerivedColors, containerId, scrollToNowPlaying, scrollToTop, view) 2 2 3 3 import Browser.Dom as Dom 4 4 import Chunky exposing (..) ··· 19 19 import Material.Icons as Icons 20 20 import Material.Icons.Types exposing (Coloring(..)) 21 21 import Maybe.Extra as Maybe 22 + import Queue 22 23 import Return3 exposing (..) 23 24 import Task 24 25 import Tracks exposing (..) 25 26 import UI.DnD as DnD 26 27 import UI.Kit 27 - import UI.Reply as UI 28 - import UI.Tracks.Reply exposing (..) 28 + import UI.Queue.Types as Queue 29 29 import UI.Tracks.Scene as Scene 30 - 31 - 32 - 33 - -- 🌳 34 - 35 - 36 - type alias Model = 37 - { dnd : DnD.Model Int 38 - , infiniteList : InfiniteList.Model 39 - } 40 - 41 - 42 - initialModel : Model 43 - initialModel = 44 - { dnd = DnD.initialModel 45 - , infiniteList = InfiniteList.init 46 - } 47 - 48 - 49 - 50 - -- 📣 51 - 52 - 53 - type Msg 54 - = Bypass 55 - | Reply Reply 56 - -- 57 - | DragAndDropMsg (DnD.Msg Int) 58 - | InfiniteListMsg InfiniteList.Model 59 - 60 - 61 - update : Msg -> Model -> Return Model Msg Reply 62 - update msg model = 63 - case msg of 64 - Bypass -> 65 - return model 66 - 67 - Reply reply -> 68 - returnReplyWithModel model reply 69 - 70 - -- 71 - InfiniteListMsg infiniteList -> 72 - return { model | infiniteList = infiniteList } 73 - 74 - DragAndDropMsg subMsg -> 75 - let 76 - -- TODO: Set { model | dragging = True } using `initiated` 77 - ( newDnD, { initiated } ) = 78 - DnD.update subMsg model.dnd 79 - in 80 - if DnD.hasDropped newDnD then 81 - returnRepliesWithModel 82 - { model | dnd = newDnD } 83 - [ MoveTrackInSelectedPlaylist 84 - { to = Maybe.withDefault 0 (DnD.modelTarget newDnD) 85 - } 86 - ] 87 - 88 - else 89 - returnRepliesWithModel 90 - { model | dnd = newDnD } 91 - [] 30 + import UI.Tracks.Types as Tracks exposing (Msg(..)) 31 + import UI.Types as UI exposing (Msg(..)) 92 32 93 33 94 34 ··· 112 52 } 113 53 114 54 115 - view : Dependencies -> List IdentifiedTrack -> InfiniteList.Model -> Bool -> Maybe IdentifiedTrack -> Maybe String -> SortBy -> SortDirection -> List Int -> Maybe (DnD.Model Int) -> Html Msg 55 + view : Dependencies -> List IdentifiedTrack -> InfiniteList.Model -> Bool -> Maybe Queue.Item -> Maybe String -> SortBy -> SortDirection -> List Int -> Maybe (DnD.Model Int) -> Html Msg 116 56 view deps harvest infiniteList favouritesOnly nowPlaying searchTerm sortBy sortDirection selectedTrackIndexes maybeDnD = 117 57 brick 118 58 ((::) ··· 192 132 "diffuse__track-list" 193 133 194 134 195 - infiniteListView : Dependencies -> List IdentifiedTrack -> InfiniteList.Model -> Bool -> Maybe String -> ( Maybe IdentifiedTrack, List Int ) -> Maybe (DnD.Model Int) -> Html Msg 135 + infiniteListView : Dependencies -> List IdentifiedTrack -> InfiniteList.Model -> Bool -> Maybe String -> ( Maybe Queue.Item, List Int ) -> Maybe (DnD.Model Int) -> Html Msg 196 136 infiniteListView deps harvest infiniteList favouritesOnly searchTerm ( nowPlaying, selectedTrackIndexes ) maybeDnD = 197 137 let 198 138 color = ··· 257 197 258 198 scrollToTop : Cmd Msg 259 199 scrollToTop = 260 - Task.attempt (always Bypass) (Dom.setViewportOf containerId 0 0) 200 + Task.attempt (always UI.Bypass) (Dom.setViewportOf containerId 0 0) 261 201 262 202 263 203 viewAttributes : List (Html.Attribute Msg) 264 204 viewAttributes = 265 - [ InfiniteList.onScroll InfiniteListMsg 205 + [ InfiniteList.onScroll (InfiniteListMsg >> TracksMsg) 266 206 , id containerId 267 207 , style "overscroll-behavior" "none" 268 208 ] ··· 323 263 324 264 else if showAlbum then 325 265 [ headerColumn "" 4.5 Nothing Bypass 326 - , headerColumn "Title" 37.5 (maybeSortIcon Title) (Reply <| SortBy Title) 327 - , headerColumn "Artist" 29.0 (maybeSortIcon Artist) (Reply <| SortBy Artist) 328 - , headerColumn "Album" 29.0 (maybeSortIcon Album) (Reply <| SortBy Album) 266 + , headerColumn "Title" 37.5 (maybeSortIcon Title) (TracksMsg <| SortBy Title) 267 + , headerColumn "Artist" 29.0 (maybeSortIcon Artist) (TracksMsg <| SortBy Artist) 268 + , headerColumn "Album" 29.0 (maybeSortIcon Album) (TracksMsg <| SortBy Album) 329 269 ] 330 270 331 271 else 332 272 [ headerColumn "" 4.5 Nothing Bypass 333 - , headerColumn "Title" 52 (maybeSortIcon Title) (Reply <| SortBy Title) 334 - , headerColumn "Artist" 43.5 (maybeSortIcon Artist) (Reply <| SortBy Artist) 273 + , headerColumn "Title" 52 (maybeSortIcon Title) (TracksMsg <| SortBy Title) 274 + , headerColumn "Artist" 43.5 (maybeSortIcon Artist) (TracksMsg <| SortBy Artist) 335 275 ] 336 276 ) 337 277 ··· 447 387 -- INFINITE LIST ITEM 448 388 449 389 450 - defaultItemView : Bool -> Maybe IdentifiedTrack -> List Int -> Bool -> DerivedColors -> Int -> Int -> IdentifiedTrack -> Html Msg 390 + defaultItemView : Bool -> Maybe Queue.Item -> List Int -> Bool -> DerivedColors -> Int -> Int -> IdentifiedTrack -> Html Msg 451 391 defaultItemView favouritesOnly nowPlaying selectedTrackIndexes showAlbum derivedColors _ idx identifiedTrack = 452 392 let 453 393 ( identifiers, track ) = ··· 466 406 467 407 rowIdentifiers = 468 408 { isMissing = identifiers.isMissing 469 - , isNowPlaying = Maybe.unwrap False (isNowPlaying identifiedTrack) nowPlaying 409 + , isNowPlaying = Maybe.unwrap False (.identifiedTrack >> isNowPlaying identifiedTrack) nowPlaying 470 410 , isSelected = isSelected 471 411 } 472 412 ··· 548 488 ] 549 489 550 490 551 - playlistItemView : Bool -> Maybe IdentifiedTrack -> Maybe String -> List Int -> DnD.Model Int -> Bool -> DerivedColors -> Int -> Int -> IdentifiedTrack -> Html Msg 491 + playlistItemView : Bool -> Maybe Queue.Item -> Maybe String -> List Int -> DnD.Model Int -> Bool -> DerivedColors -> Int -> Int -> IdentifiedTrack -> Html Msg 552 492 playlistItemView favouritesOnly nowPlaying searchTerm selectedTrackIndexes dnd showAlbum derivedColors _ idx identifiedTrack = 553 493 let 554 494 ( identifiers, track ) = ··· 559 499 560 500 dragEnv = 561 501 { model = dnd 562 - , toMsg = DragAndDropMsg 502 + , toMsg = DnD 563 503 } 564 504 565 505 isSelected = ··· 570 510 571 511 rowIdentifiers = 572 512 { isMissing = identifiers.isMissing 573 - , isNowPlaying = Maybe.unwrap False (isNowPlaying identifiedTrack) nowPlaying 513 + , isNowPlaying = Maybe.unwrap False (.identifiedTrack >> isNowPlaying identifiedTrack) nowPlaying 574 514 , isSelected = isSelected 575 515 } 576 516 ··· 673 613 else 674 614 event.clientPos 675 615 |> Coordinates.fromTuple 676 - |> ShowTrackMenuWithSmallDelay i.indexInList { alt = event.keys.alt } 677 - |> Reply 616 + |> ShowTracksMenuWithSmallDelay 617 + (Just i.indexInList) 618 + { alt = event.keys.alt } 619 + |> TracksMsg 620 + 621 + -- 678 622 , stopPropagation = True 679 623 , preventDefault = True 680 624 } ··· 697 641 698 642 Nothing -> 699 643 { x = x, y = y } 700 - |> ShowTrackMenuWithoutDelay i.indexInList { alt = False } 701 - |> Reply 644 + |> ShowTracksMenu 645 + (Just i.indexInList) 646 + { alt = False } 647 + |> TracksMsg 648 + 649 + -- 702 650 , stopPropagation = False 703 651 , preventDefault = False 704 652 } ··· 719 667 720 668 else 721 669 ( i, t ) 722 - |> UI.PlayTrack 723 - |> List.singleton 724 - |> Transcend 725 - |> Reply 670 + |> Queue.InjectFirstAndPlay 671 + |> QueueMsg 672 + 673 + -- 726 674 , stopPropagation = True 727 675 , preventDefault = True 728 676 } ··· 740 688 0 -> 741 689 { shiftKey = shiftKey } 742 690 |> MarkAsSelected i.indexInList 743 - |> Reply 691 + |> TracksMsg 744 692 745 693 _ -> 746 694 Bypass 695 + 696 + -- 747 697 , stopPropagation = True 748 698 , preventDefault = False 749 699 } ··· 806 756 ((++) 807 757 [ identifiers.indexInList 808 758 |> ToggleFavourite 809 - |> Reply 759 + |> TracksMsg 810 760 |> Html.Events.onClick 811 761 ] 812 762 (favouriteColumnStyles favouritesOnly identifiers derivedColors)
+556 -36
src/Applications/UI/Tracks/State.elm
··· 1 1 module UI.Tracks.State exposing (..) 2 2 3 + import Alien 4 + import Common exposing (..) 3 5 import ContextMenu 6 + import Coordinates exposing (Coordinates) 7 + import Html.Events.Extra.Mouse as Mouse 8 + import InfiniteList 9 + import Json.Decode as Json 10 + import Json.Encode 4 11 import List.Ext as List 5 12 import List.Extra as List 6 - import Monocle.Lens as Lens 13 + import Maybe.Extra as Maybe 7 14 import Notifications 8 - import Return 15 + import Playlists exposing (Playlist) 16 + import Return exposing (andThen, return) 9 17 import Return.Ext as Return 18 + import Sources 19 + import Task 20 + import Task.Extra as Task 21 + import Tracks exposing (..) 22 + import Tracks.Collection as Collection 23 + import Tracks.Encoding as Encoding 24 + import Tracks.Favourites as Favourites 10 25 import UI.Common.State as Common exposing (showNotification) 11 - import UI.Reply as Reply exposing (Reply(..)) 12 - import UI.Tracks as Tracks 13 - import UI.Types as UI exposing (Manager, Msg(..)) 26 + import UI.DnD as DnD 27 + import UI.Page 28 + import UI.Ports 29 + import UI.Queue.State as Queue 30 + import UI.Reply as Reply 31 + import UI.Settings.State as Settings 32 + import UI.Tracks.ContextMenu as Tracks 33 + import UI.Tracks.Scene.List 34 + import UI.Tracks.Types as Tracks exposing (..) 35 + import UI.Types as UI exposing (Manager, Model, Msg(..)) 36 + import UI.User.State.Export as User 37 + import User.Layer exposing (HypaethralData) 14 38 15 39 16 40 17 - -- 🌳 41 + -- 📣 18 42 19 43 20 - lens = 21 - { get = .tracks 22 - , set = \tracks ui -> { ui | tracks = tracks } 23 - } 44 + update : Tracks.Msg -> Manager 45 + update msg = 46 + case msg of 47 + Harvest -> 48 + harvest 49 + 50 + MarkAsSelected a b -> 51 + markAsSelected a b 52 + 53 + ScrollToNowPlaying -> 54 + scrollToNowPlaying 55 + 56 + ToggleCachedOnly -> 57 + toggleCachedOnly 58 + 59 + ToggleFavouritesOnly -> 60 + toggleFavouritesOnly 61 + 62 + ToggleHideDuplicates -> 63 + toggleHideDuplicates 64 + 65 + ----------------------------------------- 66 + -- Collection 67 + ----------------------------------------- 68 + Add a -> 69 + add a 70 + 71 + RemoveByPaths a -> 72 + removeByPaths a 73 + 74 + RemoveBySourceId a -> 75 + removeBySourceId a 76 + 77 + SortBy a -> 78 + sortBy a 79 + 80 + ToggleFavourite a -> 81 + toggleFavourite a 82 + 83 + ----------------------------------------- 84 + -- Groups 85 + ----------------------------------------- 86 + DisableGrouping -> 87 + disableGrouping 88 + 89 + GroupBy a -> 90 + groupBy a 91 + 92 + ----------------------------------------- 93 + -- Menus 94 + ----------------------------------------- 95 + ShowTracksMenu a b c -> 96 + showTracksMenu a b c 97 + 98 + ShowTracksMenuWithSmallDelay a b c -> 99 + showTracksMenuWithDelay a b c 100 + 101 + ShowViewMenu a b -> 102 + showViewMenu a b 103 + 104 + ----------------------------------------- 105 + -- Scenes 106 + ----------------------------------------- 107 + InfiniteListMsg a -> 108 + infiniteListMsg a 109 + 110 + ----------------------------------------- 111 + -- Search 112 + ----------------------------------------- 113 + ClearSearch -> 114 + clearSearch 115 + 116 + Search -> 117 + search 118 + 119 + SetSearchResults a -> 120 + setSearchResults a 121 + 122 + SetSearchTerm a -> 123 + setSearchTerm a 24 124 25 125 26 126 27 127 -- 🔱 28 128 29 129 130 + add : Json.Value -> Manager 131 + add encodedTracks = 132 + reviseCollection 133 + (encodedTracks 134 + |> Json.decodeValue (Json.list Encoding.trackDecoder) 135 + |> Result.withDefault [] 136 + |> Collection.add 137 + ) 138 + 139 + 140 + clearSearch : Manager 141 + clearSearch model = 142 + { model | searchResults = Nothing, searchTerm = Nothing } 143 + |> reviseCollection Collection.harvest 144 + |> andThen User.saveEnclosedUserData 145 + 146 + 30 147 downloadTracksFinished : Manager 31 148 downloadTracksFinished model = 32 149 case model.downloading of 33 150 Just { notificationId } -> 34 - { id = notificationId } 35 - |> DismissNotification 36 - |> Reply 37 - |> Return.performanceF { model | downloading = Nothing } 151 + Common.dismissNotification 152 + { id = notificationId } 153 + { model | downloading = Nothing } 38 154 39 155 Nothing -> 40 156 Return.singleton model 41 157 42 158 159 + disableGrouping : Manager 160 + disableGrouping model = 161 + { model | grouping = Nothing } 162 + |> reviseCollection Collection.arrange 163 + |> andThen User.saveEnclosedUserData 164 + 165 + 43 166 failedToStoreTracksInCache : List String -> Manager 44 - failedToStoreTracksInCache trackIds model = 45 - model 46 - |> Lens.modify lens 47 - (\m -> { m | cachingInProgress = List.without trackIds m.cachingInProgress }) 48 - |> showNotification 49 - (Notifications.error "Failed to store track in cache") 167 + failedToStoreTracksInCache trackIds m = 168 + showNotification 169 + (Notifications.error "Failed to store track in cache") 170 + { m | cachingTracksInProgress = List.without trackIds m.cachingTracksInProgress } 50 171 51 172 52 173 finishedStoringTracksInCache : List String -> Manager 53 174 finishedStoringTracksInCache trackIds model = 54 - model 55 - |> Lens.modify lens 56 - (\t -> 57 - { t 58 - | cached = t.cached ++ trackIds 59 - , cachingInProgress = List.without trackIds t.cachingInProgress 60 - } 61 - ) 175 + { model 176 + | cachedTracks = model.cachedTracks ++ trackIds 177 + , cachingTracksInProgress = List.without trackIds model.cachingTracksInProgress 178 + } 62 179 |> (\m -> 63 180 -- When a context menu of a track is open, 64 181 -- it should be "rerendered" in case ··· 75 192 ContextMenu.coordinates contextMenu 76 193 in 77 194 if isTrackContextMenu then 78 - m.tracks.collection.harvested 79 - |> List.pickIndexes m.tracks.selectedTrackIndexes 80 - |> ShowTracksContextMenu coordinates { alt = False } 81 - |> Reply 82 - |> Return.performanceF m 195 + showTracksMenu Nothing { alt = False } coordinates m 83 196 84 197 else 85 198 Return.singleton m ··· 87 200 Nothing -> 88 201 Return.singleton m 89 202 ) 90 - -- TODO: Make sync 91 - |> Return.effect_ (\_ -> Return.task <| TracksMsg Tracks.Harvest) 92 - |> Return.effect_ (\_ -> Return.task <| Reply Reply.SaveEnclosedUserData) 203 + |> andThen harvest 204 + |> andThen User.saveEnclosedUserData 205 + 206 + 207 + groupBy : Tracks.Grouping -> Manager 208 + groupBy grouping model = 209 + { model | grouping = Just grouping } 210 + |> reviseCollection Collection.arrange 211 + |> andThen User.saveEnclosedUserData 212 + 213 + 214 + harvest : Manager 215 + harvest = 216 + reviseCollection Collection.harvest 217 + 218 + 219 + infiniteListMsg : InfiniteList.Model -> Manager 220 + infiniteListMsg infiniteList model = 221 + Return.singleton { model | infiniteList = infiniteList } 222 + 223 + 224 + markAsSelected : Int -> { shiftKey : Bool } -> Manager 225 + markAsSelected indexInList { shiftKey } model = 226 + let 227 + selection = 228 + if shiftKey then 229 + model.selectedTrackIndexes 230 + |> List.head 231 + |> Maybe.map 232 + (\n -> 233 + if n > indexInList then 234 + List.range indexInList n 235 + 236 + else 237 + List.range n indexInList 238 + ) 239 + |> Maybe.withDefault [ indexInList ] 240 + 241 + else 242 + [ indexInList ] 243 + in 244 + Return.singleton { model | selectedTrackIndexes = selection } 245 + 246 + 247 + removeByPaths : Json.Value -> Manager 248 + removeByPaths encodedParams model = 249 + let 250 + decoder = 251 + Json.map2 252 + Tuple.pair 253 + (Json.field "filePaths" <| Json.list Json.string) 254 + (Json.field "sourceId" Json.string) 255 + 256 + ( paths, sourceId ) = 257 + encodedParams 258 + |> Json.decodeValue decoder 259 + |> Result.withDefault ( [], missingId ) 260 + 261 + { kept, removed } = 262 + Tracks.removeByPaths 263 + { sourceId = sourceId, paths = paths } 264 + model.tracks.untouched 265 + 266 + newCollection = 267 + { emptyCollection | untouched = kept } 268 + in 269 + { model | tracks = newCollection } 270 + |> reviseCollection Collection.identify 271 + |> andThen (Return.performance <| Reply <| Reply.RemoveTracksFromCache removed) 272 + 273 + 274 + removeBySourceId : String -> Manager 275 + removeBySourceId sourceId model = 276 + let 277 + { kept, removed } = 278 + Tracks.removeBySourceId sourceId model.tracks.untouched 279 + 280 + newCollection = 281 + { emptyCollection | untouched = kept } 282 + in 283 + { model | tracks = newCollection } 284 + |> reviseCollection Collection.identify 285 + |> andThen (Return.performance <| Reply <| Reply.RemoveTracksFromCache removed) 286 + 287 + 288 + reviseCollection : (Parcel -> Parcel) -> Manager 289 + reviseCollection collector model = 290 + resolveParcel 291 + (model 292 + |> makeParcel 293 + |> collector 294 + ) 295 + model 296 + 297 + 298 + search : Manager 299 + search model = 300 + case ( model.searchTerm, model.searchResults ) of 301 + ( Just term, _ ) -> 302 + term 303 + |> String.trim 304 + |> Json.Encode.string 305 + |> UI.Ports.giveBrain Alien.SearchTracks 306 + |> return model 307 + 308 + ( Nothing, Just _ ) -> 309 + reviseCollection Collection.harvest { model | searchResults = Nothing } 310 + 311 + ( Nothing, Nothing ) -> 312 + Return.singleton model 313 + 314 + 315 + setSearchResults : Json.Value -> Manager 316 + setSearchResults json model = 317 + case model.searchTerm of 318 + Just _ -> 319 + json 320 + |> Json.decodeValue (Json.list Json.string) 321 + |> Result.withDefault [] 322 + |> (\results -> { model | searchResults = Just results }) 323 + |> reviseCollection Collection.harvest 324 + |> andThen (Common.toggleLoadingScreen Off) 325 + 326 + Nothing -> 327 + Return.singleton model 328 + 329 + 330 + setSearchTerm : String -> Manager 331 + setSearchTerm term model = 332 + User.saveEnclosedUserData 333 + (case String.trim term of 334 + "" -> 335 + { model | searchTerm = Nothing } 336 + 337 + _ -> 338 + { model | searchTerm = Just term } 339 + ) 340 + 341 + 342 + showTracksMenu : Maybe Int -> { alt : Bool } -> Coordinates -> Manager 343 + showTracksMenu maybeTrackIndex { alt } coordinates model = 344 + let 345 + selection = 346 + case maybeTrackIndex of 347 + Just trackIndex -> 348 + if List.isEmpty model.selectedTrackIndexes then 349 + [ trackIndex ] 350 + 351 + else if List.member trackIndex model.selectedTrackIndexes == False then 352 + [ trackIndex ] 353 + 354 + else 355 + model.selectedTrackIndexes 356 + 357 + Nothing -> 358 + model.selectedTrackIndexes 359 + 360 + menuDependencies = 361 + { cached = model.cachedTracks 362 + , cachingInProgress = model.cachingTracksInProgress 363 + , currentTime = model.currentTime 364 + , selectedPlaylist = model.selectedPlaylist 365 + , lastModifiedPlaylistName = model.lastModifiedPlaylist 366 + , showAlternativeMenu = alt 367 + , sources = model.sources 368 + } 369 + 370 + tracks = 371 + List.pickIndexes selection model.tracks.harvested 372 + in 373 + coordinates 374 + |> Tracks.trackMenu menuDependencies tracks 375 + |> Common.showContextMenuWithModel 376 + { model 377 + | dnd = DnD.initialModel 378 + , selectedTrackIndexes = selection 379 + } 380 + 381 + 382 + showTracksMenuWithDelay : Maybe Int -> { alt : Bool } -> Coordinates -> Manager 383 + showTracksMenuWithDelay a b c model = 384 + Tracks.ShowTracksMenu a b c 385 + |> TracksMsg 386 + |> Task.doDelayed 250 387 + |> return model 388 + 389 + 390 + showViewMenu : Maybe Grouping -> Mouse.Event -> Manager 391 + showViewMenu maybeGrouping mouseEvent model = 392 + mouseEvent.clientPos 393 + |> Coordinates.fromTuple 394 + |> Tracks.viewMenu model.cachedTracksOnly maybeGrouping 395 + |> Common.showContextMenuWithModel model 396 + 397 + 398 + scrollToNowPlaying : Manager 399 + scrollToNowPlaying model = 400 + model.nowPlaying 401 + |> Maybe.map 402 + (.identifiedTrack >> Tuple.second >> .id) 403 + |> Maybe.andThen 404 + (\id -> 405 + List.find 406 + (Tuple.second >> .id >> (==) id) 407 + model.tracks.harvested 408 + ) 409 + |> Maybe.map 410 + (case model.scene of 411 + List -> 412 + UI.Tracks.Scene.List.scrollToNowPlaying model.tracks.harvested 413 + ) 414 + |> Maybe.map 415 + (\cmd -> 416 + cmd 417 + |> return model 418 + |> andThen (Common.changeUrlUsingPage UI.Page.Index) 419 + ) 420 + |> Maybe.withDefault 421 + (Return.singleton model) 422 + 423 + 424 + sortBy : SortBy -> Manager 425 + sortBy property model = 426 + let 427 + sortDir = 428 + if model.sortBy /= property then 429 + Asc 430 + 431 + else if model.sortDirection == Asc then 432 + Desc 433 + 434 + else 435 + Asc 436 + in 437 + { model | sortBy = property, sortDirection = sortDir } 438 + |> reviseCollection Collection.arrange 439 + |> andThen User.saveEnclosedUserData 440 + 441 + 442 + toggleCachedOnly : Manager 443 + toggleCachedOnly model = 444 + { model | cachedTracksOnly = not model.cachedTracksOnly } 445 + |> reviseCollection Collection.harvest 446 + |> andThen User.saveEnclosedUserData 447 + 448 + 449 + toggleFavourite : Int -> Manager 450 + toggleFavourite index model = 451 + case List.getAt index model.tracks.harvested of 452 + Just ( i, t ) -> 453 + let 454 + newFavourites = 455 + Favourites.toggleInFavouritesList ( i, t ) model.favourites 456 + 457 + effect = 458 + if model.favouritesOnly then 459 + Collection.map (Favourites.toggleInTracksList t) >> Collection.harvest 460 + 461 + else 462 + Collection.map (Favourites.toggleInTracksList t) 463 + in 464 + { model | favourites = newFavourites } 465 + |> reviseCollection effect 466 + |> andThen (Return.performance <| Reply Reply.SaveFavourites) 467 + 468 + Nothing -> 469 + Return.singleton model 470 + 471 + 472 + toggleFavouritesOnly : Manager 473 + toggleFavouritesOnly model = 474 + { model | favouritesOnly = not model.favouritesOnly } 475 + |> reviseCollection Collection.harvest 476 + |> andThen User.saveEnclosedUserData 477 + 478 + 479 + toggleHideDuplicates : Manager 480 + toggleHideDuplicates model = 481 + { model | hideDuplicates = not model.hideDuplicates } 482 + |> reviseCollection Collection.arrange 483 + |> andThen Settings.save 484 + 485 + 486 + 487 + -- 📣 ░░ PARCEL 488 + 489 + 490 + makeParcel : Model -> Parcel 491 + makeParcel model = 492 + ( { cached = model.cachedTracks 493 + , cachedOnly = model.cachedTracksOnly 494 + , enabledSourceIds = Sources.enabledSourceIds model.sources 495 + , favourites = model.favourites 496 + , favouritesOnly = model.favouritesOnly 497 + , grouping = model.grouping 498 + , hideDuplicates = model.hideDuplicates 499 + , searchResults = model.searchResults 500 + , selectedPlaylist = model.selectedPlaylist 501 + , sortBy = model.sortBy 502 + , sortDirection = model.sortDirection 503 + } 504 + , model.tracks 505 + ) 506 + 507 + 508 + resolveParcel : Parcel -> Manager 509 + resolveParcel ( deps, newCollection ) model = 510 + let 511 + scrollObj = 512 + Json.Encode.object 513 + [ ( "scrollTop", Json.Encode.int 0 ) ] 514 + 515 + scrollEvent = 516 + Json.Encode.object 517 + [ ( "target", scrollObj ) ] 518 + 519 + newScrollContext = 520 + scrollContext model 521 + 522 + collectionChanged = 523 + Collection.tracksChanged 524 + model.tracks.untouched 525 + newCollection.untouched 526 + 527 + harvestChanged = 528 + Collection.harvestChanged 529 + model.tracks.harvested 530 + newCollection.harvested 531 + 532 + searchChanged = 533 + newScrollContext /= model.tracks.scrollContext 534 + 535 + modelWithNewCollection = 536 + (if model.scene == List && searchChanged then 537 + \m -> { m | infiniteList = InfiniteList.updateScroll scrollEvent m.infiniteList } 538 + 539 + else 540 + identity 541 + ) 542 + { model 543 + | tracks = 544 + { newCollection | scrollContext = newScrollContext } 545 + , selectedTrackIndexes = 546 + if collectionChanged || harvestChanged then 547 + [] 548 + 549 + else 550 + model.selectedTrackIndexes 551 + } 552 + in 553 + (if collectionChanged then 554 + andThen Common.generateDirectoryPlaylists >> andThen Queue.reset 555 + 556 + else if harvestChanged then 557 + andThen Queue.reset 558 + 559 + else 560 + identity 561 + ) 562 + ( modelWithNewCollection 563 + ----------------------------------------- 564 + -- Command 565 + ----------------------------------------- 566 + , if searchChanged then 567 + case model.scene of 568 + List -> 569 + UI.Tracks.Scene.List.scrollToTop 570 + 571 + else 572 + Cmd.none 573 + ) 574 + 575 + 576 + scrollContext : Model -> String 577 + scrollContext model = 578 + String.concat 579 + [ Maybe.withDefault "" <| model.searchTerm 580 + , Maybe.withDefault "" <| Maybe.map .name model.selectedPlaylist 581 + ] 582 + 583 + 584 + 585 + -- 📣 ░░ USER DATA 586 + 587 + 588 + importHypaethral : HypaethralData -> Maybe Playlist -> Manager 589 + importHypaethral data selectedPlaylist model = 590 + let 591 + adjustedModel = 592 + { model 593 + | favourites = data.favourites 594 + , hideDuplicates = Maybe.unwrap False .hideDuplicates data.settings 595 + , selectedPlaylist = selectedPlaylist 596 + , tracks = { emptyCollection | untouched = data.tracks } 597 + } 598 + in 599 + adjustedModel 600 + |> resolveParcel 601 + (adjustedModel 602 + |> makeParcel 603 + |> Collection.identify 604 + ) 605 + |> andThen search 606 + |> (case model.searchTerm of 607 + Just _ -> 608 + identity 609 + 610 + Nothing -> 611 + andThen (Common.toggleLoadingScreen Off) 612 + )
+58
src/Applications/UI/Tracks/Types.elm
··· 1 + module UI.Tracks.Types exposing (..) 2 + 3 + import Coordinates exposing (Coordinates) 4 + import Html.Events.Extra.Mouse as Mouse 5 + import InfiniteList 6 + import Json.Decode as Json 7 + import Tracks exposing (..) 8 + 9 + 10 + 11 + -- 🌳 12 + 13 + 14 + type Scene 15 + = List 16 + 17 + 18 + 19 + -- 📣 20 + 21 + 22 + type Msg 23 + = Harvest 24 + | MarkAsSelected Int { shiftKey : Bool } 25 + | ScrollToNowPlaying 26 + | ToggleCachedOnly 27 + | ToggleFavouritesOnly 28 + | ToggleHideDuplicates 29 + ----------------------------------------- 30 + -- Collection 31 + ----------------------------------------- 32 + | Add Json.Value 33 + | RemoveByPaths Json.Value 34 + | RemoveBySourceId String 35 + | SortBy SortBy 36 + | ToggleFavourite Int 37 + ----------------------------------------- 38 + -- Groups 39 + ----------------------------------------- 40 + | DisableGrouping 41 + | GroupBy Grouping 42 + ----------------------------------------- 43 + -- Menus 44 + ----------------------------------------- 45 + | ShowTracksMenu (Maybe Int) { alt : Bool } Coordinates 46 + | ShowTracksMenuWithSmallDelay (Maybe Int) { alt : Bool } Coordinates 47 + | ShowViewMenu (Maybe Grouping) Mouse.Event 48 + ----------------------------------------- 49 + -- Scenes 50 + ----------------------------------------- 51 + | InfiniteListMsg InfiniteList.Model 52 + ----------------------------------------- 53 + -- Search 54 + ----------------------------------------- 55 + | ClearSearch 56 + | Search 57 + | SetSearchResults Json.Value 58 + | SetSearchTerm String
+401
src/Applications/UI/Tracks/View.elm
··· 1 + module UI.Tracks.View exposing (view) 2 + 3 + import Chunky exposing (..) 4 + import Color exposing (Color) 5 + import Color.Ext as Color 6 + import Common exposing (Switch(..)) 7 + import Conditional exposing (ifThenElse) 8 + import Coordinates exposing (Viewport) 9 + import Css.Classes as C 10 + import Html exposing (Html, text) 11 + import Html.Attributes exposing (href, placeholder, style, tabindex, target, title, value) 12 + import Html.Events exposing (onBlur, onClick, onInput) 13 + import Html.Events.Extra.Mouse as Mouse 14 + import Html.Ext exposing (onEnterKey) 15 + import Html.Lazy exposing (..) 16 + import List.Ext as List 17 + import List.Extra as List 18 + import Material.Icons as Icons 19 + import Material.Icons.Types exposing (Coloring(..)) 20 + import Maybe.Extra as Maybe 21 + import Playlists exposing (Playlist) 22 + import Return3 as Return exposing (..) 23 + import Sources 24 + import Tracks exposing (..) 25 + import Tracks.Collection as Collection exposing (..) 26 + import UI.Kit 27 + import UI.Navigation exposing (..) 28 + import UI.Page 29 + import UI.Playlists.Page 30 + import UI.Queue.Page 31 + import UI.Reply as UI exposing (Reply(..)) 32 + import UI.Sources.Page as Sources 33 + import UI.Tracks.Scene.List 34 + import UI.Tracks.Types as Tracks exposing (..) 35 + import UI.Types as UI exposing (..) 36 + import User.Layer exposing (HypaethralData) 37 + 38 + 39 + 40 + -- 🗺 41 + 42 + 43 + type alias Dependencies = 44 + { amountOfSources : Int 45 + , bgColor : Maybe Color 46 + , darkMode : Bool 47 + , isOnIndexPage : Bool 48 + , isTouchDevice : Bool 49 + , sourceIdsBeingProcessed : List String 50 + , viewport : Viewport 51 + } 52 + 53 + 54 + view : Model -> Dependencies -> Html UI.Msg 55 + view model deps = 56 + chunk 57 + viewClasses 58 + [ lazy6 59 + navigation 60 + model.grouping 61 + model.favouritesOnly 62 + model.searchTerm 63 + model.selectedPlaylist 64 + deps.isOnIndexPage 65 + deps.bgColor 66 + 67 + -- 68 + , if List.isEmpty model.tracks.harvested then 69 + lazy4 70 + noTracksView 71 + deps.sourceIdsBeingProcessed 72 + deps.amountOfSources 73 + (List.length model.tracks.harvested) 74 + (List.length model.favourites) 75 + 76 + else 77 + case model.scene of 78 + List -> 79 + listView model deps 80 + ] 81 + 82 + 83 + viewClasses : List String 84 + viewClasses = 85 + [ C.flex 86 + , C.flex_col 87 + , C.flex_grow 88 + ] 89 + 90 + 91 + navigation : Maybe Grouping -> Bool -> Maybe String -> Maybe Playlist -> Bool -> Maybe Color -> Html UI.Msg 92 + navigation maybeGrouping favouritesOnly searchTerm selectedPlaylist isOnIndexPage bgColor = 93 + let 94 + tabindex_ = 95 + ifThenElse isOnIndexPage 0 -1 96 + in 97 + chunk 98 + [ C.flex ] 99 + [ ----------------------------------------- 100 + -- Part 1 101 + ----------------------------------------- 102 + chunk 103 + [ C.border_b 104 + , C.border_r 105 + , C.border_gray_300 106 + , C.flex 107 + , C.flex_grow 108 + , C.mt_px 109 + , C.overflow_hidden 110 + , C.relative 111 + , C.text_gray_600 112 + 113 + -- Dark mode 114 + ------------ 115 + , C.dark__border_base01 116 + , C.dark__text_base04 117 + ] 118 + [ -- Input 119 + -------- 120 + slab 121 + Html.input 122 + [ onBlur (TracksMsg Search) 123 + , onEnterKey (TracksMsg Search) 124 + , onInput (TracksMsg << SetSearchTerm) 125 + , placeholder "Search" 126 + , tabindex tabindex_ 127 + , value (Maybe.withDefault "" searchTerm) 128 + ] 129 + [ C.bg_transparent 130 + , C.border_none 131 + , C.flex_grow 132 + , C.h_full 133 + , C.ml_1 134 + , C.mt_px 135 + , C.outline_none 136 + , C.pl_8 137 + , C.pr_2 138 + , C.pt_px 139 + , C.text_base02 140 + , C.text_sm 141 + , C.w_full 142 + 143 + -- Dark mode 144 + ------------ 145 + , C.dark__text_base06 146 + ] 147 + [] 148 + 149 + -- Search icon 150 + -------------- 151 + , chunk 152 + [ C.absolute 153 + , C.bottom_0 154 + , C.flex 155 + , C.items_center 156 + , C.left_0 157 + , C.ml_3 158 + , C.mt_px 159 + , C.top_0 160 + , C.z_0 161 + ] 162 + [ Icons.search 16 Inherit ] 163 + 164 + -- Actions 165 + ---------- 166 + , chunk 167 + [ C.flex 168 + , C.items_center 169 + , C.mr_3 170 + , C.mt_px 171 + , C.pt_px 172 + ] 173 + [ -- 1 174 + case searchTerm of 175 + Just _ -> 176 + brick 177 + [ onClick (TracksMsg ClearSearch) 178 + , title "Clear search" 179 + ] 180 + [ C.cursor_pointer 181 + , C.ml_1 182 + , C.mt_px 183 + ] 184 + [ Icons.clear 16 Inherit ] 185 + 186 + Nothing -> 187 + nothing 188 + 189 + -- 2 190 + , brick 191 + [ onClick (TracksMsg ToggleFavouritesOnly) 192 + , title "Toggle favourites-only" 193 + ] 194 + [ C.cursor_pointer 195 + , C.ml_1 196 + ] 197 + [ case favouritesOnly of 198 + True -> 199 + Icons.favorite 16 (Color UI.Kit.colorKit.base08) 200 + 201 + False -> 202 + Icons.favorite_border 16 Inherit 203 + ] 204 + 205 + -- 3 206 + , brick 207 + [ Mouse.onClick (TracksMsg << ShowViewMenu maybeGrouping) 208 + , title "View settings" 209 + ] 210 + [ C.cursor_pointer 211 + , C.ml_1 212 + ] 213 + [ Icons.more_vert 16 Inherit ] 214 + 215 + -- 4 216 + , case selectedPlaylist of 217 + Just playlist -> 218 + brick 219 + [ onClick DeselectPlaylist 220 + 221 + -- 222 + , bgColor 223 + |> Maybe.withDefault UI.Kit.colorKit.base01 224 + |> Color.toCssString 225 + |> style "background-color" 226 + ] 227 + [ C.antialiased 228 + , C.cursor_pointer 229 + , C.duration_500 230 + , C.font_bold 231 + , C.leading_none 232 + , C.ml_1 233 + , C.px_1 234 + , C.py_1 235 + , C.rounded 236 + , C.truncate 237 + , C.text_white_90 238 + , C.text_xxs 239 + , C.transition 240 + 241 + -- Dark mode 242 + ------------ 243 + , C.dark__text_white_60 244 + ] 245 + [ chunk 246 + [ C.px_px, C.pt_px ] 247 + [ text playlist.name ] 248 + ] 249 + 250 + Nothing -> 251 + nothing 252 + ] 253 + ] 254 + , ----------------------------------------- 255 + -- Part 2 256 + ----------------------------------------- 257 + UI.Navigation.localWithTabindex 258 + tabindex_ 259 + [ ( Icon Icons.waves 260 + , Label "Playlists" Hidden 261 + , NavigateToPage (UI.Page.Playlists UI.Playlists.Page.Index) 262 + ) 263 + , ( Icon Icons.schedule 264 + , Label "Queue" Hidden 265 + , NavigateToPage (UI.Page.Queue UI.Queue.Page.Index) 266 + ) 267 + , ( Icon Icons.equalizer 268 + , Label "Equalizer" Hidden 269 + , NavigateToPage UI.Page.Equalizer 270 + ) 271 + ] 272 + ] 273 + 274 + 275 + noTracksView : List String -> Int -> Int -> Int -> Html UI.Msg 276 + noTracksView processingContext amountOfSources amountOfTracks amountOfFavourites = 277 + chunk 278 + [ C.flex, C.flex_grow ] 279 + [ UI.Kit.centeredContent 280 + [ if List.length processingContext > 0 then 281 + message "Processing Tracks" 282 + 283 + else if amountOfSources == 0 then 284 + chunk 285 + [ C.flex 286 + , C.flex_wrap 287 + , C.items_start 288 + , C.justify_center 289 + , C.px_3 290 + ] 291 + [ -- Add 292 + ------ 293 + inline 294 + [ C.mb_3, C.mx_2, C.whitespace_no_wrap ] 295 + [ UI.Kit.buttonLink 296 + (Sources.NewOnboarding 297 + |> UI.Page.Sources 298 + |> UI.Page.toString 299 + ) 300 + UI.Kit.Filled 301 + (buttonContents 302 + [ UI.Kit.inlineIcon Icons.add 303 + , text "Add some music" 304 + ] 305 + ) 306 + ] 307 + 308 + -- Demo 309 + ------- 310 + , inline 311 + [ C.mb_3, C.mx_2, C.whitespace_no_wrap ] 312 + [ UI.Kit.buttonWithColor 313 + UI.Kit.Gray 314 + UI.Kit.Normal 315 + (Reply InsertDemo) 316 + (buttonContents 317 + [ UI.Kit.inlineIcon Icons.music_note 318 + , text "Insert demo" 319 + ] 320 + ) 321 + ] 322 + 323 + -- How 324 + ------ 325 + , inline 326 + [ C.mb_3, C.mx_2, C.whitespace_no_wrap ] 327 + [ UI.Kit.buttonWithOptions 328 + Html.a 329 + [ href "about" 330 + , target "_blank" 331 + ] 332 + UI.Kit.Gray 333 + UI.Kit.Normal 334 + Nothing 335 + (buttonContents 336 + [ UI.Kit.inlineIcon Icons.help 337 + , text "More info" 338 + ] 339 + ) 340 + ] 341 + ] 342 + 343 + else if amountOfTracks == 0 then 344 + message "No tracks found" 345 + 346 + else 347 + message "No sources available" 348 + ] 349 + ] 350 + 351 + 352 + buttonContents : List (Html UI.Msg) -> Html UI.Msg 353 + buttonContents = 354 + inline 355 + [ C.flex 356 + , C.items_center 357 + , C.leading_0 358 + ] 359 + 360 + 361 + message : String -> Html UI.Msg 362 + message m = 363 + chunk 364 + [ C.border_b_2 365 + , C.border_current_color 366 + , C.text_sm 367 + , C.font_semibold 368 + , C.leading_snug 369 + , C.pb_1 370 + ] 371 + [ text m ] 372 + 373 + 374 + listView : Model -> Dependencies -> Html UI.Msg 375 + listView model deps = 376 + model.selectedPlaylist 377 + |> Maybe.map .autoGenerated 378 + |> Maybe.andThen 379 + (\bool -> 380 + if bool then 381 + Nothing 382 + 383 + else 384 + Just model.dnd 385 + ) 386 + |> UI.Tracks.Scene.List.view 387 + { bgColor = deps.bgColor 388 + , darkMode = deps.darkMode 389 + , height = deps.viewport.height 390 + , isTouchDevice = deps.isTouchDevice 391 + , isVisible = deps.isOnIndexPage 392 + , showAlbum = deps.viewport.width >= 720 393 + } 394 + model.tracks.harvested 395 + model.infiniteList 396 + model.favouritesOnly 397 + model.nowPlaying 398 + model.searchTerm 399 + model.sortBy 400 + model.sortDirection 401 + model.selectedTrackIndexes
+30 -7
src/Applications/UI/Types.elm
··· 17 17 import Html.Events.Extra.Mouse as Mouse 18 18 import Html.Events.Extra.Pointer as Pointer 19 19 import Http 20 + import InfiniteList 20 21 import Json.Decode 21 22 import Keyboard 22 23 import LastFm ··· 27 28 import Sources exposing (Source) 28 29 import Sources.Encoding as Sources 29 30 import Time 30 - import Tracks 31 + import Tracks exposing (..) 31 32 import Tracks.Encoding as Tracks 32 33 import UI.Authentication.Types as Authentication 33 34 import UI.DnD as DnD ··· 37 38 import UI.Reply as Reply exposing (Reply(..)) 38 39 import UI.Sources.ContextMenu as Sources 39 40 import UI.Sources.Types as Sources 40 - import UI.Tracks as Tracks 41 41 import UI.Tracks.ContextMenu as Tracks 42 + import UI.Tracks.Types as Tracks exposing (Scene) 42 43 import Url exposing (Protocol(..), Url) 43 44 import User.Layer exposing (..) 44 45 import User.Layer.Methods.RemoteStorage as RemoteStorage ··· 126 127 , newPlaylistContext : Maybe String 127 128 , playlists : List Playlist 128 129 , playlistToActivate : Maybe String 130 + , selectedPlaylist : Maybe Playlist 129 131 130 132 ----------------------------------------- 131 133 -- Queue ··· 136 138 , playingNext : List Queue.Item 137 139 , selectedQueueItem : Maybe Queue.Item 138 140 139 - -- 141 + -- Settings 142 + ----------- 140 143 , repeat : Bool 141 144 , shuffle : Bool 142 145 ··· 150 153 , sources : List Source 151 154 152 155 ----------------------------------------- 153 - -- 🦉 Nested 156 + -- Tracks 154 157 ----------------------------------------- 155 - , authentication : Authentication.State 158 + , cachedTracks : List String 159 + , cachedTracksOnly : Bool 160 + , cachingTracksInProgress : List String 161 + , favourites : List Favourite 162 + , favouritesOnly : Bool 163 + , grouping : Maybe Grouping 164 + , hideDuplicates : Bool 165 + , scene : Scene 166 + , searchResults : Maybe (List String) 167 + , searchTerm : Maybe String 168 + , selectedTrackIndexes : List Int 169 + , sortBy : SortBy 170 + , sortDirection : SortDirection 171 + , tracks : Tracks.Collection 172 + 173 + -- List scene 174 + ------------- 175 + , infiniteList : InfiniteList.Model 156 176 157 177 ----------------------------------------- 158 - -- Children (TODO) 178 + -- 🦉 Nested 159 179 ----------------------------------------- 160 - , tracks : Tracks.Model 180 + , authentication : Authentication.State 161 181 } 162 182 163 183 ··· 229 249 | CreatePlaylist 230 250 | DeactivatePlaylist 231 251 | DeletePlaylist { playlistName : String } 252 + | DeselectPlaylist 232 253 | ModifyPlaylist 254 + | MoveTrackInSelectedPlaylist { to : Int } 255 + | SelectPlaylist Playlist 233 256 | SetPlaylistCreationContext String 234 257 | SetPlaylistModificationContext String String 235 258 | ShowPlaylistListMenu Playlist Mouse.Event
+8 -8
src/Applications/UI/User/State/Export.elm
··· 13 13 14 14 saveEnclosedUserData : Manager 15 15 saveEnclosedUserData model = 16 - { cachedTracks = model.tracks.cached 16 + { cachedTracks = model.cachedTracks 17 17 , equalizerSettings = model.eqSettings 18 - , grouping = model.tracks.grouping 19 - , onlyShowCachedTracks = model.tracks.cachedOnly 20 - , onlyShowFavourites = model.tracks.favouritesOnly 18 + , grouping = model.grouping 19 + , onlyShowCachedTracks = model.cachedTracksOnly 20 + , onlyShowFavourites = model.favouritesOnly 21 21 , repeat = model.repeat 22 - , searchTerm = model.tracks.searchTerm 23 - , selectedPlaylist = Maybe.map .name model.tracks.selectedPlaylist 22 + , searchTerm = model.searchTerm 23 + , selectedPlaylist = Maybe.map .name model.selectedPlaylist 24 24 , shuffle = model.shuffle 25 - , sortBy = model.tracks.sortBy 26 - , sortDirection = model.tracks.sortDirection 25 + , sortBy = model.sortBy 26 + , sortDirection = model.sortDirection 27 27 } 28 28 |> encodeEnclosedData 29 29 |> Alien.broadcast Alien.SaveEnclosedUserData
+26 -45
src/Applications/UI/User/State/Import.elm
··· 21 21 import UI.Reply exposing (..) 22 22 import UI.Reply.Translate as Reply 23 23 import UI.Sources.State as Sources 24 - import UI.Tracks as Tracks 24 + import UI.Tracks.State as Tracks 25 25 import UI.Types as UI exposing (..) 26 26 import Url.Ext as Url 27 27 import User.Layer exposing (..) ··· 47 47 |> Json.Decode.decodeString Json.Decode.value 48 48 |> Result.withDefault Json.Encode.null 49 49 |> (\j -> importHypaethral j model) 50 - |> Return3.wield Reply.translate 51 50 -- Show notification 52 51 |> andThen 53 52 ("Imported data successfully!" ··· 75 74 loadHypaethralUserData json model = 76 75 model 77 76 |> importHypaethral json 78 - |> Return3.wield Reply.translate 79 77 |> andThen 80 78 (\m -> 81 79 case Url.action m.url of ··· 117 115 -- ⚗️ ░░ HYPAETHRAL DATA 118 116 119 117 120 - importHypaethral : Json.Decode.Value -> Model -> Return3.Return Model Msg Reply 118 + importHypaethral : Json.Decode.Value -> Manager 121 119 importHypaethral value model = 122 120 case decodeHypaethralData value of 123 121 Ok data -> 124 122 let 125 - { sources } = 126 - model 127 - 128 123 chosenBackdrop = 129 124 data.settings 130 125 |> Maybe.andThen .backgroundImage ··· 141 136 (\n -> List.find (.name >> (==) n) newPlaylistsCollection) 142 137 model.playlistToActivate 143 138 144 - ( tracksModel, tracksCmd, tracksReplies ) = 145 - Tracks.importHypaethral model.tracks data selectedPlaylist 146 - 147 139 lastFmModel = 148 140 model.lastFm 149 141 in 150 - ( { model 151 - | tracks = tracksModel 152 - 153 - -- 154 - , chosenBackdrop = chosenBackdrop 155 - , lastFm = { lastFmModel | sessionKey = Maybe.andThen .lastFm data.settings } 156 - , playlists = newPlaylistsCollection 157 - , playlistToActivate = Nothing 158 - , processAutomatically = Maybe.unwrap True .processAutomatically data.settings 159 - , progress = data.progress 160 - , rememberProgress = Maybe.unwrap True .rememberProgress data.settings 161 - , sources = data.sources 162 - } 163 - -- 164 - , Cmd.map TracksMsg tracksCmd 165 - -- 166 - , tracksReplies 167 - ) 142 + Tracks.importHypaethral 143 + data 144 + selectedPlaylist 145 + { model 146 + | chosenBackdrop = chosenBackdrop 147 + , lastFm = { lastFmModel | sessionKey = Maybe.andThen .lastFm data.settings } 148 + , playlists = newPlaylistsCollection 149 + , playlistToActivate = Nothing 150 + , processAutomatically = Maybe.unwrap True .processAutomatically data.settings 151 + , progress = data.progress 152 + , rememberProgress = Maybe.unwrap True .rememberProgress data.settings 153 + , sources = data.sources 154 + } 168 155 169 156 Err err -> 170 157 err 171 158 |> Json.Decode.errorToString 172 - |> ShowErrorNotification 173 - |> Return3.returnReplyWithModel model 159 + |> Notifications.error 160 + |> Common.showNotificationWithModel model 174 161 175 162 176 163 ··· 180 167 importEnclosed : Json.Decode.Value -> Model -> Return3.Return Model Msg Reply 181 168 importEnclosed value model = 182 169 let 183 - { tracks } = 184 - model 185 - 186 170 equalizerSettings = 187 171 model.eqSettings 188 172 in ··· 196 180 , high = data.equalizerSettings.high 197 181 , volume = data.equalizerSettings.volume 198 182 } 199 - 200 - newTracks = 201 - { tracks 202 - | cached = data.cachedTracks 203 - , cachedOnly = data.onlyShowCachedTracks 204 - , favouritesOnly = data.onlyShowFavourites 205 - , grouping = data.grouping 206 - , searchTerm = data.searchTerm 207 - , sortBy = data.sortBy 208 - , sortDirection = data.sortDirection 209 - } 210 183 in 211 184 ( { model 212 185 | eqSettings = newEqualizerSettings 213 186 , playlistToActivate = data.selectedPlaylist 214 187 , repeat = data.repeat 215 188 , shuffle = data.shuffle 216 - , tracks = newTracks 189 + 190 + -- Tracks 191 + , cachedTracks = data.cachedTracks 192 + , cachedTracksOnly = data.onlyShowCachedTracks 193 + , favouritesOnly = data.onlyShowFavourites 194 + , grouping = data.grouping 195 + , searchTerm = data.searchTerm 196 + , sortBy = data.sortBy 197 + , sortDirection = data.sortDirection 217 198 } 218 199 -- 219 200 , Cmd.batch
+14 -14
src/Applications/UI/View.elm
··· 45 45 import UI.Sources.Page 46 46 import UI.Sources.View as Sources 47 47 import UI.Svg.Elements 48 - import UI.Tracks as Tracks 49 48 import UI.Tracks.ContextMenu as Tracks 49 + import UI.Tracks.View as Tracks 50 50 import UI.Types exposing (..) 51 51 import Url exposing (Protocol(..)) 52 52 import User.Layer exposing (..) ··· 85 85 else if Maybe.isJust model.selectedQueueItem then 86 86 [ on "tap" (Json.Decode.succeed RemoveQueueSelection) ] 87 87 88 - else if not (List.isEmpty model.tracks.selectedTrackIndexes) then 88 + else if not (List.isEmpty model.selectedTrackIndexes) then 89 89 [ on "tap" (Json.Decode.succeed RemoveTrackSelection) ] 90 90 91 91 else ··· 158 158 -- Main 159 159 ----------------------------------------- 160 160 , vessel 161 - [ { amountOfSources = List.length model.sources 162 - , bgColor = model.extractedBackdropColor 163 - , darkMode = model.darkMode 164 - , isOnIndexPage = model.page == Page.Index 165 - , isTouchDevice = model.isTouchDevice 166 - , sourceIdsBeingProcessed = List.map Tuple.first model.processingContext 167 - , viewport = model.viewport 168 - } 169 - |> Tracks.view model.tracks 170 - |> Html.map TracksMsg 161 + [ Tracks.view 162 + model 163 + { amountOfSources = List.length model.sources 164 + , bgColor = model.extractedBackdropColor 165 + , darkMode = model.darkMode 166 + , isOnIndexPage = model.page == Page.Index 167 + , isTouchDevice = model.isTouchDevice 168 + , sourceIdsBeingProcessed = List.map Tuple.first model.processingContext 169 + , viewport = model.viewport 170 + } 171 171 172 172 -- Pages 173 173 -------- ··· 183 183 Playlists.view 184 184 subPage 185 185 model.playlists 186 - model.tracks.selectedPlaylist 186 + model.selectedPlaylist 187 187 model.editPlaylistContext 188 188 model.extractedBackdropColor 189 189 ··· 193 193 Page.Settings subPage -> 194 194 { authenticationMethod = Authentication.extractMethod model.authentication 195 195 , chosenBackgroundImage = model.chosenBackdrop 196 - , hideDuplicateTracks = model.tracks.hideDuplicates 196 + , hideDuplicateTracks = model.hideDuplicates 197 197 , lastFm = model.lastFm 198 198 , processAutomatically = model.processAutomatically 199 199 , rememberProgress = model.rememberProgress