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

+996 -850
+27 -31
src/Applications/UI.elm
··· 46 46 import UI.Reply.Translate as Reply 47 47 import UI.Routing.State as Routing 48 48 import UI.Services.State as Services 49 - import UI.Sources as Sources 50 49 import UI.Sources.ContextMenu as Sources 50 + import UI.Sources.Form 51 + import UI.Sources.State as Sources 51 52 import UI.Tracks as Tracks 52 53 import UI.Tracks.ContextMenu as Tracks 53 54 import UI.Tracks.State as Tracks ··· 174 175 , shuffle = False 175 176 176 177 ----------------------------------------- 178 + -- Sources 179 + ----------------------------------------- 180 + , processingContext = [] 181 + , processingError = Nothing 182 + , processingNotificationId = Nothing 183 + , sources = [] 184 + , sourceForm = UI.Sources.Form.initialModel 185 + 186 + ----------------------------------------- 177 187 -- 🦉 Nested 178 188 ----------------------------------------- 179 189 , authentication = Authentication.initialModel url ··· 181 191 ----------------------------------------- 182 192 -- Children (TODO) 183 193 ----------------------------------------- 184 - , sources = Sources.initialModel 185 194 , tracks = Tracks.initialModel 186 195 } 187 196 |> Routing.transition page ··· 248 257 Audio.playPause 249 258 250 259 ----------------------------------------- 251 - -- Authentication 260 + -- Authentication (TODO: Move) 252 261 ----------------------------------------- 253 262 AuthenticationBootFailure a -> 254 263 Authentication.bootFailure a ··· 365 374 -- Routing 366 375 ----------------------------------------- 367 376 ChangeUrlUsingPage a -> 368 - Routing.changeUrlUsingPage a 377 + Common.changeUrlUsingPage a 369 378 370 379 LinkClicked a -> 371 380 Routing.linkClicked a ··· 386 395 Services.scrobble a 387 396 388 397 ----------------------------------------- 389 - -- Tracks 398 + -- Tracks (TODO: Move) 390 399 ----------------------------------------- 391 400 DownloadTracksFinished -> 392 401 Tracks.downloadTracksFinished ··· 422 431 Adjunct.keyboardInput a 423 432 424 433 ----------------------------------------- 425 - -- 📭 Other 426 - ----------------------------------------- 427 - SetCurrentTime a -> 428 - Other.setCurrentTime a 429 - 430 - SetIsOnline a -> 431 - Other.setIsOnline a 432 - 433 - ----------------------------------------- 434 434 -- 🦉 Nested 435 435 ----------------------------------------- 436 436 AuthenticationMsg a -> ··· 439 439 QueueMsg a -> 440 440 Queue.update a 441 441 442 + SourcesMsg a -> 443 + Sources.update a 444 + 445 + ----------------------------------------- 446 + -- 📭 Other 447 + ----------------------------------------- 448 + SetCurrentTime a -> 449 + Other.setCurrentTime a 450 + 451 + SetIsOnline a -> 452 + Other.setIsOnline a 453 + 442 454 ----------------------------------------- 443 455 -- Children (TODO) 444 456 ----------------------------------------- 445 - SourcesMsg sub -> 446 - \model -> 447 - Return3.wieldNested 448 - Reply.translate 449 - { mapCmd = SourcesMsg 450 - , mapModel = \child -> { model | sources = child } 451 - , update = Sources.update 452 - } 453 - { model = model.sources 454 - , msg = sub 455 - } 456 - 457 457 TracksMsg sub -> 458 458 \model -> 459 459 Return3.wieldNested ··· 496 496 ----------------------------------------- 497 497 -- Interface 498 498 ----------------------------------------- 499 + , Browser.Events.onResize Interface.onResize 499 500 , Ports.indicateTouchDevice (\_ -> SetIsTouchDevice True) 500 501 , Ports.preferredColorSchemaChanged PreferredColorSchemaChanged 501 502 , Ports.showErrorNotification (Notifications.error >> ShowNotification) ··· 507 508 , Ports.activeQueueItemEnded (QueueMsg << always Queue.Shift) 508 509 , Ports.requestNext (\_ -> QueueMsg Queue.Shift) 509 510 , Ports.requestPrevious (\_ -> QueueMsg Queue.Rewind) 510 - 511 - ----------------------------------------- 512 - -- Resize 513 - ----------------------------------------- 514 - , Browser.Events.onResize Interface.onResize 515 511 516 512 ----------------------------------------- 517 513 -- Services
+2 -1
src/Applications/UI/Alien.elm
··· 5 5 import Json.Decode 6 6 import Notifications 7 7 import UI.Authentication.Types as Authentication 8 - import UI.Sources as Sources 8 + import UI.Sources.Types as Sources 9 9 import UI.Tracks as Tracks 10 10 import UI.Types exposing (..) 11 11 import User.Layer exposing (..) ··· 46 46 Just Alien.FinishedProcessingSource -> 47 47 event.data 48 48 |> Json.Decode.decodeValue Json.Decode.string 49 + |> Result.map (\id -> { sourceId = id }) 49 50 |> Result.map (Sources.FinishedProcessingSource >> SourcesMsg) 50 51 |> Result.withDefault Bypass 51 52
+25 -1
src/Applications/UI/Common/State.elm
··· 1 1 module UI.Common.State exposing (..) 2 2 3 + import Browser.Navigation as Nav 4 + import ContextMenu exposing (ContextMenu) 3 5 import Monocle.Lens as Lens exposing (Lens) 4 6 import Notifications exposing (Notification) 5 - import Return 7 + import Return exposing (return) 6 8 import UI.Notifications 9 + import UI.Page as Page exposing (Page) 7 10 import UI.Reply exposing (Reply) 8 11 import UI.Types as UI exposing (Manager) 9 12 10 13 11 14 12 15 -- 📣 16 + 17 + 18 + changeUrlUsingPage : Page -> Manager 19 + changeUrlUsingPage page model = 20 + page 21 + |> Page.toString 22 + |> Nav.pushUrl model.navKey 23 + |> return model 24 + 25 + 26 + dismissNotification : { id : Int } -> Manager 27 + dismissNotification options model = 28 + options 29 + |> UI.Notifications.dismiss model.notifications 30 + |> Return.map (\n -> { model | notifications = n }) 31 + |> Return.mapCmd UI.Reply 32 + 33 + 34 + showContextMenuWithModel : UI.Model -> ContextMenu Reply -> ( UI.Model, Cmd UI.Msg ) 35 + showContextMenuWithModel model contextMenu = 36 + Return.singleton { model | contextMenu = Just contextMenu } 13 37 14 38 15 39 showNotification : Notification Reply -> Manager
+1 -4
src/Applications/UI/Other/State.elm
··· 54 54 55 55 setCurrentTime : Time.Posix -> Manager 56 56 setCurrentTime time model = 57 - model 58 - |> (\m -> { m | currentTime = time }) 59 - |> Lens.modify Sources.lens (\s -> { s | currentTime = time }) 60 - |> Return.singleton 57 + Return.singleton { model | currentTime = time } 61 58 62 59 63 60
+1 -2
src/Applications/UI/Playlists/State.elm
··· 16 16 import UI.Playlists.ContextMenu as Playlists 17 17 import UI.Playlists.Page exposing (..) 18 18 import UI.Ports as Ports 19 - import UI.Routing.State as Routing 20 19 import UI.Tracks as Tracks 21 20 import UI.Types as UI exposing (..) 22 21 ··· 230 229 231 230 redirectToIndexPage : Manager 232 231 redirectToIndexPage = 233 - Routing.changeUrlUsingPage (Page.Playlists Index) 232 + Common.changeUrlUsingPage (Page.Playlists Index)
+2 -2
src/Applications/UI/Queue/State.elm
··· 87 87 |> Maybe.map 88 88 (Queue.makeEngineItem 89 89 model.currentTime 90 - model.sources.collection 90 + model.sources 91 91 model.tracks.cached 92 92 (if model.rememberProgress then 93 93 model.progress ··· 168 168 |> Tuple.second 169 169 |> Queue.makeEngineItem 170 170 model.currentTime 171 - model.sources.collection 171 + model.sources 172 172 model.tracks.cached 173 173 (if model.rememberProgress then 174 174 model.progress
-2
src/Applications/UI/Reply.elm
··· 41 41 | ReplyViaContextMenu Reply 42 42 | ShowMoreAuthenticationOptions Coordinates 43 43 | ShowPlaylistListMenu Coordinates Playlist 44 - | ShowSourceContextMenu Coordinates Source 45 44 | ShowTracksContextMenu Coordinates { alt : Bool } (List IdentifiedTrack) 46 45 | ShowTracksViewMenu Coordinates (Maybe Tracks.Grouping) 47 46 ----------------------------------------- ··· 98 97 | RemoveSourceFromCollection { sourceId : String } 99 98 | RemoveTracksFromCache (List Track) 100 99 | RemoveTracksWithSourceId String 101 - | ReplaceSourceInCollection Source 102 100 | ScrollToNowPlaying 103 101 | StoreTracksInCache (List Track) 104 102 | ToggleCachedTracksOnly
+16 -76
src/Applications/UI/Reply/Translate.elm
··· 33 33 import UI.Authentication.ContextMenu as Authentication 34 34 import UI.Authentication.Types as Authentication 35 35 import UI.Backdrop as Backdrop 36 - import UI.Common.State exposing (showNotification, showNotificationWithModel) 36 + import UI.Common.State as Common exposing (showNotification, showNotificationWithModel) 37 37 import UI.Demo as Demo 38 38 import UI.Interface.State as Interface 39 39 import UI.Notifications ··· 45 45 import UI.Queue.State as Queue 46 46 import UI.Queue.Types as Queue 47 47 import UI.Reply as Reply exposing (Reply(..)) 48 - import UI.Routing.State as Routing 49 48 import UI.Settings as Settings 50 - import UI.Sources as Sources 51 49 import UI.Sources.ContextMenu as Sources 50 + import UI.Sources.State as Sources 51 + import UI.Sources.Types as Sources 52 52 import UI.Tracks as Tracks 53 53 import UI.Tracks.ContextMenu as Tracks 54 54 import UI.Tracks.Scene.List ··· 77 77 |> return model 78 78 79 79 GoToPage page -> 80 - Routing.changeUrlUsingPage page model 80 + Common.changeUrlUsingPage page model 81 81 82 82 Reply.ToggleLoadingScreen a -> 83 83 Interface.toggleLoadingScreen a model ··· 162 162 , shuffle = False 163 163 164 164 -- 165 - , sources = 166 - { sources 167 - | collection = [] 168 - , isProcessing = [] 169 - } 165 + , processingContext = [] 166 + , sources = [] 167 + 168 + -- 170 169 , tracks = 171 170 { tracks 172 171 | collection = Tracks.emptyCollection ··· 205 204 Reply.ShowPlaylistListMenu coordinates playlist -> 206 205 Return.singleton { model | contextMenu = Just (Playlists.listMenu playlist model.tracks.collection.identified model.confirmation coordinates) } 207 206 208 - ShowSourceContextMenu coordinates source -> 209 - Return.singleton { model | contextMenu = Just (Sources.sourceMenu source coordinates) } 210 - 211 207 ShowTracksContextMenu coordinates { alt } tracks -> 212 208 let 213 209 menuDependencies = ··· 217 213 , selectedPlaylist = model.tracks.selectedPlaylist 218 214 , lastModifiedPlaylistName = model.lastModifiedPlaylist 219 215 , showAlternativeMenu = alt 220 - , sources = model.sources.collection 216 + , sources = model.sources 221 217 } 222 218 in 223 219 Return.singleton { model | contextMenu = Just (Tracks.trackMenu menuDependencies tracks coordinates) } ··· 309 305 310 306 directoryPlaylists = 311 307 UI.Playlists.Directory.generate 312 - model.sources.collection 308 + model.sources 313 309 model.tracks.collection.untouched 314 310 in 315 311 [ nonDirectoryPlaylists ··· 464 460 |> TracksMsg 465 461 |> Return.performanceF model 466 462 467 - ProcessSources [] -> 468 - Return.singleton model 469 - 470 - ProcessSources sourcesToProcess -> 471 - let 472 - notification = 473 - Notifications.stickyWarning "Processing sources ..." 474 - 475 - notificationId = 476 - Notifications.id notification 477 - 478 - newNotifications = 479 - List.filter 480 - (\n -> Notifications.kind n /= Notifications.Error) 481 - model.notifications 482 - 483 - sources = 484 - model.sources 485 - 486 - isProcessing = 487 - sourcesToProcess 488 - |> List.sortBy (.data >> Dict.fetch "name" "") 489 - |> List.map (\{ id } -> ( id, 0 )) 490 - 491 - newSources = 492 - { sources 493 - | isProcessing = isProcessing 494 - , processingError = Nothing 495 - , processingNotificationId = Just notificationId 496 - } 497 - 498 - newModel = 499 - { model | notifications = newNotifications, sources = newSources } 500 - in 501 - [ ( "origin" 502 - , Json.Encode.string (Common.urlOrigin model.url) 503 - ) 504 - , ( "sources" 505 - , Json.Encode.list Sources.encode sourcesToProcess 506 - ) 507 - ] 508 - |> Json.Encode.object 509 - |> Alien.broadcast Alien.ProcessSources 510 - |> Ports.toBrain 511 - |> return newModel 512 - |> andThen (showNotification notification) 463 + ProcessSources sources -> 464 + Sources.process model 513 465 514 466 RemoveSourceFromCollection args -> 515 467 args ··· 548 500 |> Return.performanceF model 549 501 |> Return.command cmd 550 502 551 - ReplaceSourceInCollection source -> 552 - let 553 - sources = 554 - model.sources 555 - in 556 - model.sources.collection 557 - |> List.map (\s -> ifThenElse (s.id == source.id) source s) 558 - |> (\c -> { sources | collection = c }) 559 - |> (\s -> { model | sources = s }) 560 - |> Return.singleton 561 - |> andThen (translate SaveSources) 562 - 563 503 ScrollToNowPlaying -> 564 504 Return.performance (TracksMsg Tracks.ScrollToNowPlaying) model 565 505 ··· 592 532 , track 593 533 |> Queue.makeTrackUrl 594 534 model.currentTime 595 - model.sources.collection 535 + model.sources 596 536 |> Json.Encode.string 597 537 ) 598 538 ] ··· 631 571 , playlists = List.filterNot .autoGenerated model.playlists 632 572 , progress = model.progress 633 573 , settings = Just (gatherSettings model) 634 - , sources = model.sources.collection 574 + , sources = model.sources 635 575 , tracks = model.tracks.collection.untouched 636 576 } 637 577 |> encodeHypaethralData ··· 690 630 SaveSources -> 691 631 let 692 632 updateEnabledSourceIdsOnTracks = 693 - model.sources.collection 633 + model.sources 694 634 |> Sources.enabledSourceIds 695 635 |> Tracks.SetEnabledSourceIds 696 636 |> TracksMsg ··· 699 639 ( updatedModel, updatedCmd ) = 700 640 updateEnabledSourceIdsOnTracks model 701 641 in 702 - updatedModel.sources.collection 642 + updatedModel.sources 703 643 |> Json.Encode.list Sources.encode 704 644 |> Alien.broadcast Alien.SaveSources 705 645 |> Ports.toBrain
+9 -17
src/Applications/UI/Routing/State.elm
··· 1 - module UI.Routing.State exposing (changeUrlUsingPage, linkClicked, resetUrl, transition, urlChanged) 1 + module UI.Routing.State exposing (linkClicked, resetUrl, transition, urlChanged) 2 2 3 3 import Browser exposing (UrlRequest) 4 4 import Browser.Navigation as Nav ··· 9 9 import Sources 10 10 import Sources.Services.Dropbox 11 11 import Sources.Services.Google 12 + import UI.Common.State as Common 12 13 import UI.Page as Page exposing (Page) 13 14 import UI.Sources.Form 14 15 import UI.Sources.Page 15 16 import UI.Sources.State as Sources 17 + import UI.Sources.Types as Sources 16 18 import UI.Types as UI exposing (Manager) 17 19 import Url exposing (Url) 18 20 19 21 20 22 21 23 -- 🔱 22 - 23 - 24 - changeUrlUsingPage : Page -> Manager 25 - changeUrlUsingPage page model = 26 - page 27 - |> Page.toString 28 - |> Nav.pushUrl model.navKey 29 - |> return model 30 24 31 25 32 26 linkClicked : UrlRequest -> Manager ··· 80 74 ----------------------------------------- 81 75 Page.Sources (UI.Sources.Page.NewThroughRedirect service args) -> 82 76 let 83 - ( sources, form, defaultContext ) = 84 - ( model.sources 85 - , model.sources.form 77 + ( form, defaultContext ) = 78 + ( model.sourceForm 86 79 , UI.Sources.Form.defaultContext 87 80 ) 88 81 in ··· 100 93 , service = 101 94 service 102 95 } 103 - |> (\c -> { form | context = c, step = UI.Sources.Form.By }) 104 - |> (\f -> { sources | form = f }) 105 - |> (\s -> { model | sources = s }) 96 + |> (\c -> { form | context = c, step = Sources.By }) 97 + |> (\f -> { model | sourceForm = f }) 106 98 |> Return.singleton 107 99 108 100 ----------------------------------------- ··· 144 136 model.isLoading 145 137 146 138 maybeSource = 147 - List.find (.id >> (==) sourceId) model.sources.collection 139 + List.find (.id >> (==) sourceId) model.sources 148 140 in 149 141 case ( isLoading, maybeSource ) of 150 142 ( False, Just source ) -> ··· 157 149 158 150 ( True, _ ) -> 159 151 -- Redirect away from edit-source page 160 - changeUrlUsingPage 152 + Common.changeUrlUsingPage 161 153 (Page.Sources UI.Sources.Page.Index) 162 154 model
-516
src/Applications/UI/Sources.elm
··· 1 - module UI.Sources exposing (Model, Msg(..), initialModel, sourcesToProcess, update, view) 2 - 3 - import Alien 4 - import Chunky exposing (..) 5 - import Conditional exposing (ifThenElse) 6 - import Coordinates 7 - import Css.Classes as C 8 - import Dict.Ext as Dict 9 - import Html exposing (Html, text) 10 - import Html.Attributes exposing (href) 11 - import Html.Events.Extra.Mouse as Mouse 12 - import Json.Decode as Json 13 - import List.Extra as List 14 - import Material.Icons as Icons 15 - import Material.Icons.Types exposing (Coloring(..)) 16 - import Return3 as Return exposing (..) 17 - import Sources exposing (..) 18 - import Sources.Encoding 19 - import Time 20 - import Time.Ext as Time 21 - import UI.Kit exposing (ButtonType(..)) 22 - import UI.List 23 - import UI.Navigation exposing (..) 24 - import UI.Page as Page 25 - import UI.Ports as Ports 26 - import UI.Reply exposing (Reply(..)) 27 - import UI.Sources.Form as Form 28 - import UI.Sources.Page as Sources exposing (..) 29 - 30 - 31 - 32 - -- 🌳 33 - 34 - 35 - type alias Model = 36 - { collection : List Source 37 - , currentTime : Time.Posix 38 - , form : Form.Model 39 - , isProcessing : List ( String, Float ) 40 - , processingError : Maybe { error : String, sourceId : String } 41 - , processingNotificationId : Maybe Int 42 - } 43 - 44 - 45 - initialModel : Model 46 - initialModel = 47 - { collection = [] 48 - , currentTime = Time.default 49 - , form = Form.initialModel 50 - , isProcessing = [] 51 - , processingError = Nothing 52 - , processingNotificationId = Nothing 53 - } 54 - 55 - 56 - 57 - -- 📣 58 - 59 - 60 - type Msg 61 - = Bypass 62 - | FinishedProcessingSource String 63 - | FinishedProcessing 64 - | Process 65 - | ReportProcessingError Json.Value 66 - | ReportProcessingProgress Json.Value 67 - | StopProcessing 68 - ----------------------------------------- 69 - -- Children 70 - ----------------------------------------- 71 - | FormMsg Form.Msg 72 - ----------------------------------------- 73 - -- Collection 74 - ----------------------------------------- 75 - | AddToCollection Source 76 - | RemoveFromCollection { sourceId : String } 77 - | UpdateSourceData Json.Value 78 - ----------------------------------------- 79 - -- Individual 80 - ----------------------------------------- 81 - | SourceContextMenu Source Mouse.Event 82 - | ToggleActivation { sourceId : String } 83 - | ToggleDirectoryPlaylists { sourceId : String } 84 - 85 - 86 - update : Msg -> Model -> Return Model Msg Reply 87 - update msg model = 88 - case msg of 89 - Bypass -> 90 - return model 91 - 92 - FinishedProcessing -> 93 - model.processingNotificationId 94 - |> Maybe.map (\id -> [ DismissNotification { id = id } ]) 95 - |> Maybe.withDefault [] 96 - |> Return.repliesWithModel { model | isProcessing = [] } 97 - 98 - FinishedProcessingSource sourceId -> 99 - return { model | isProcessing = List.filter (Tuple.first >> (/=) sourceId) model.isProcessing } 100 - 101 - Process -> 102 - model 103 - |> sourcesToProcess 104 - |> ProcessSources 105 - |> returnReplyWithModel model 106 - 107 - ReportProcessingError json -> 108 - case Json.decodeValue (Json.dict Json.string) json of 109 - Ok dict -> 110 - let 111 - args = 112 - { error = Dict.fetch "error" "" dict 113 - , sourceId = Dict.fetch "sourceId" "" dict 114 - } 115 - in 116 - dict 117 - |> Dict.fetch "error" "missingError" 118 - |> ShowStickyErrorNotificationWithCode 119 - ("Could not process the _" 120 - ++ Dict.fetch "sourceName" "" dict 121 - ++ "_ source. I got the following response from the source:" 122 - ) 123 - |> returnReplyWithModel 124 - { model | processingError = Just args } 125 - 126 - Err _ -> 127 - "Could not decode processing error" 128 - |> ShowStickyErrorNotification 129 - |> returnReplyWithModel model 130 - 131 - ReportProcessingProgress json -> 132 - case 133 - Json.decodeValue 134 - (Json.map2 135 - (\p s -> 136 - { progress = p 137 - , sourceId = s 138 - } 139 - ) 140 - (Json.field "progress" Json.float) 141 - (Json.field "sourceId" Json.string) 142 - ) 143 - json 144 - of 145 - Ok { progress, sourceId } -> 146 - model.isProcessing 147 - |> List.map 148 - (\( sid, pro ) -> 149 - ifThenElse (sid == sourceId) 150 - ( sid, progress ) 151 - ( sid, pro ) 152 - ) 153 - |> (\isProcessing -> 154 - { model | isProcessing = isProcessing } 155 - ) 156 - |> return 157 - 158 - Err _ -> 159 - "Could not decode processing progress" 160 - |> ShowStickyErrorNotification 161 - |> returnReplyWithModel model 162 - 163 - StopProcessing -> 164 - case model.processingNotificationId of 165 - Just notificationId -> 166 - Alien.StopProcessing 167 - |> Alien.trigger 168 - |> Ports.toBrain 169 - |> returnCommandWithModel 170 - { model 171 - | isProcessing = [] 172 - , processingNotificationId = Nothing 173 - } 174 - |> addReply (DismissNotification { id = notificationId }) 175 - 176 - Nothing -> 177 - return model 178 - 179 - ----------------------------------------- 180 - -- Children 181 - ----------------------------------------- 182 - FormMsg sub -> 183 - model.form 184 - |> Form.update sub 185 - |> mapModel (\f -> { model | form = f }) 186 - |> mapCmd FormMsg 187 - 188 - ----------------------------------------- 189 - -- Collection 190 - ----------------------------------------- 191 - AddToCollection unsuitableSource -> 192 - let 193 - source = 194 - setProperId 195 - (List.length model.collection + 1) 196 - model.currentTime 197 - unsuitableSource 198 - in 199 - returnRepliesWithModel 200 - { model | collection = model.collection ++ [ source ] } 201 - [ UI.Reply.SaveSources 202 - , UI.Reply.ProcessSources [ source ] 203 - ] 204 - 205 - RemoveFromCollection { sourceId } -> 206 - model.collection 207 - |> List.filter (.id >> (/=) sourceId) 208 - |> (\c -> { model | collection = c }) 209 - |> return 210 - |> addReplies 211 - [ UI.Reply.SaveSources 212 - , UI.Reply.RemoveTracksWithSourceId sourceId 213 - ] 214 - 215 - UpdateSourceData json -> 216 - json 217 - |> Sources.Encoding.decode 218 - |> Maybe.map 219 - (\source -> 220 - List.map 221 - (\s -> 222 - if s.id == source.id then 223 - source 224 - 225 - else 226 - s 227 - ) 228 - model.collection 229 - ) 230 - |> Maybe.map (\col -> { model | collection = col }) 231 - |> Maybe.withDefault model 232 - |> return 233 - |> addReply UI.Reply.SaveSources 234 - 235 - ----------------------------------------- 236 - -- Individual 237 - ----------------------------------------- 238 - SourceContextMenu source mouseEvent -> 239 - returnRepliesWithModel 240 - model 241 - [ ShowSourceContextMenu 242 - (Coordinates.fromTuple mouseEvent.clientPos) 243 - source 244 - ] 245 - 246 - ToggleActivation { sourceId } -> 247 - model.collection 248 - |> List.map 249 - (\source -> 250 - if source.id == sourceId then 251 - { source | enabled = not source.enabled } 252 - 253 - else 254 - source 255 - ) 256 - |> (\collection -> { model | collection = collection }) 257 - |> return 258 - |> addReply SaveSources 259 - 260 - ToggleDirectoryPlaylists { sourceId } -> 261 - model.collection 262 - |> List.map 263 - (\source -> 264 - if source.id == sourceId then 265 - { source | directoryPlaylists = not source.directoryPlaylists } 266 - 267 - else 268 - source 269 - ) 270 - |> (\collection -> { model | collection = collection }) 271 - |> return 272 - |> addReply SaveSources 273 - |> addReply GenerateDirectoryPlaylists 274 - 275 - 276 - sourcesToProcess : Model -> List Source 277 - sourcesToProcess model = 278 - List.filter (.enabled >> (==) True) model.collection 279 - 280 - 281 - 282 - -- 🗺 283 - 284 - 285 - view : { amountOfTracks : Int } -> Sources.Page -> Model -> Html Msg 286 - view { amountOfTracks } page model = 287 - UI.Kit.receptacle 288 - { scrolling = True } 289 - (case page of 290 - Index -> 291 - index amountOfTracks model 292 - 293 - Edit sourceId -> 294 - List.map (Html.map FormMsg) (Form.edit model.form) 295 - 296 - New -> 297 - List.map (Html.map FormMsg) (Form.new { onboarding = False } model.form) 298 - 299 - NewOnboarding -> 300 - List.map (Html.map FormMsg) (Form.new { onboarding = True } model.form) 301 - 302 - NewThroughRedirect _ _ -> 303 - List.map (Html.map FormMsg) (Form.new { onboarding = False } model.form) 304 - 305 - Rename sourceId -> 306 - List.map (Html.map FormMsg) (Form.rename model.form) 307 - ) 308 - 309 - 310 - 311 - -- INDEX 312 - 313 - 314 - index : Int -> Model -> List (Html Msg) 315 - index amountOfTracks model = 316 - [ ----------------------------------------- 317 - -- Navigation 318 - ----------------------------------------- 319 - if List.isEmpty model.collection then 320 - UI.Navigation.local 321 - [ ( Icon Icons.add 322 - , Label "Add a new source" Shown 323 - , NavigateToPage (Page.Sources New) 324 - ) 325 - ] 326 - 327 - else 328 - UI.Navigation.local 329 - [ ( Icon Icons.add 330 - , Label "Add a new source" Shown 331 - , NavigateToPage (Page.Sources New) 332 - ) 333 - 334 - -- Process 335 - ---------- 336 - , if List.isEmpty model.isProcessing then 337 - ( Icon Icons.sync 338 - , Label "Process sources" Shown 339 - , PerformMsg Process 340 - ) 341 - 342 - else 343 - ( Icon Icons.sync 344 - , Label "Stop processing ..." Shown 345 - , PerformMsg StopProcessing 346 - ) 347 - ] 348 - 349 - ----------------------------------------- 350 - -- Content 351 - ----------------------------------------- 352 - , if List.isEmpty model.collection then 353 - chunk 354 - [ C.relative ] 355 - [ chunk 356 - [ C.absolute, C.left_0, C.top_0 ] 357 - [ UI.Kit.canister [ UI.Kit.h1 "Sources" ] ] 358 - ] 359 - 360 - else 361 - UI.Kit.canister 362 - [ UI.Kit.h1 "Sources" 363 - 364 - -- Intro 365 - -------- 366 - , intro amountOfTracks 367 - 368 - -- List 369 - ------- 370 - , model.collection 371 - |> List.sortBy 372 - (.data >> Dict.fetch "name" "") 373 - |> List.map 374 - (\source -> 375 - { label = Html.text (Dict.fetch "name" "" source.data) 376 - , actions = sourceActions model.isProcessing model.processingError source 377 - , msg = Nothing 378 - , isSelected = False 379 - } 380 - ) 381 - |> UI.List.view UI.List.Normal 382 - ] 383 - 384 - -- 385 - , if List.isEmpty model.collection then 386 - UI.Kit.centeredContent 387 - [ slab 388 - Html.a 389 - [ href (Page.toString <| Page.Sources New) ] 390 - [ C.block 391 - , C.opacity_30 392 - , C.text_inherit 393 - ] 394 - [ Icons.music_note 64 Inherit ] 395 - , slab 396 - Html.a 397 - [ href (Page.toString <| Page.Sources New) ] 398 - [ C.block 399 - , C.leading_normal 400 - , C.mt_2 401 - , C.opacity_40 402 - , C.text_center 403 - , C.text_inherit 404 - ] 405 - [ text "A source is a place where music is stored," 406 - , lineBreak 407 - , text "add one so you can play some music " 408 - , inline 409 - [ C.align_middle, C.inline_block, C.minus_mt_px ] 410 - [ Icons.add 14 Inherit ] 411 - ] 412 - ] 413 - 414 - else 415 - nothing 416 - ] 417 - 418 - 419 - intro : Int -> Html Msg 420 - intro amountOfTracks = 421 - [ text "A source is a place where your music is stored." 422 - , lineBreak 423 - , text "By connecting a source, the application will scan it and keep a list of all the music in it." 424 - , lineBreak 425 - , text "You currently have " 426 - , text (String.fromInt amountOfTracks) 427 - , text " " 428 - , text (ifThenElse (amountOfTracks == 1) "track" "tracks") 429 - , text " in your collection." 430 - ] 431 - |> raw 432 - |> UI.Kit.intro 433 - 434 - 435 - sourceActions : List ( String, Float ) -> Maybe { error : String, sourceId : String } -> Source -> List (UI.List.Action Msg) 436 - sourceActions isProcessing processingError source = 437 - let 438 - processIndex = 439 - List.findIndex (Tuple.first >> (==) source.id) isProcessing 440 - 441 - process = 442 - Maybe.andThen (\idx -> List.getAt idx isProcessing) processIndex 443 - in 444 - List.append 445 - (case ( process, processingError ) of 446 - ( Just ( _, progress ), _ ) -> 447 - [ { icon = 448 - \_ _ -> 449 - if progress < 0.05 then 450 - inline 451 - [ C.inline_block, C.opacity_70, C.px_1 ] 452 - [ case processIndex of 453 - Just 0 -> 454 - Html.text "Listing" 455 - 456 - _ -> 457 - Html.text "Waiting" 458 - ] 459 - 460 - else 461 - progress 462 - |> (*) 100 463 - |> round 464 - |> String.fromInt 465 - |> (\s -> s ++ "%") 466 - |> Html.text 467 - |> List.singleton 468 - |> inline [ C.inline_block, C.opacity_70, C.px_1 ] 469 - , msg = Nothing 470 - , title = "" 471 - } 472 - , { icon = Icons.sync 473 - , msg = Nothing 474 - , title = "Currently processing" 475 - } 476 - ] 477 - 478 - ( Nothing, Just { error, sourceId } ) -> 479 - if sourceId == source.id then 480 - [ { icon = \size _ -> Icons.error_outline size (Color UI.Kit.colors.error) 481 - , msg = Nothing 482 - , title = error 483 - } 484 - ] 485 - 486 - else 487 - [] 488 - 489 - _ -> 490 - [] 491 - ) 492 - [ { icon = 493 - if source.enabled then 494 - Icons.check 495 - 496 - else 497 - Icons.block 498 - , msg = 499 - { sourceId = source.id } 500 - |> ToggleActivation 501 - |> always 502 - |> Just 503 - , title = 504 - if source.enabled then 505 - "Enabled (click to disable)" 506 - 507 - else 508 - "Disabled (click to enable)" 509 - } 510 - 511 - -- 512 - , { icon = Icons.more_vert 513 - , msg = Just (SourceContextMenu source) 514 - , title = "Menu" 515 - } 516 - ]
+17 -154
src/Applications/UI/Sources/Form.elm
··· 1 - module UI.Sources.Form exposing (FormStep(..), Model, Msg(..), defaultContext, edit, initialModel, new, rename, takeStepBackwards, takeStepForwards, update) 1 + module UI.Sources.Form exposing (..) 2 2 3 3 import Chunky exposing (..) 4 4 import Common exposing (boolFromString, boolToString) ··· 22 22 import UI.Page as Page 23 23 import UI.Reply exposing (Reply(..)) 24 24 import UI.Sources.Page as Sources 25 + import UI.Sources.Types exposing (..) 26 + import UI.Types exposing (Model) 25 27 26 28 27 29 28 30 -- 🌳 29 31 30 32 31 - type alias Model = 32 - { step : FormStep 33 - , context : Source 34 - } 35 - 36 - 37 - type FormStep 38 - = Where 39 - | How 40 - | By 41 - 42 - 43 - initialModel : Model 33 + initialModel : Form 44 34 initialModel = 45 35 { step = Where 46 36 , context = defaultContext ··· 63 53 64 54 65 55 66 - -- 📣 67 - 68 - 69 - type Msg 70 - = AddSource 71 - | Bypass 72 - | EditSource 73 - | RenameSource 74 - | ReturnToIndex 75 - | SelectService String 76 - | SetData String String 77 - | TakeStep 78 - | TakeStepBackwards 79 - 80 - 81 - update : Msg -> Model -> Return Model Msg Reply 82 - update msg model = 83 - case msg of 84 - AddSource -> 85 - let 86 - context = 87 - model.context 88 - 89 - cleanContext = 90 - { context | data = Dict.map (always String.trim) context.data } 91 - in 92 - returnRepliesWithModel 93 - { model | step = Where, context = defaultContext } 94 - [ GoToPage (Page.Sources Sources.Index) 95 - , AddSourceToCollection cleanContext 96 - ] 97 - 98 - Bypass -> 99 - return model 100 - 101 - EditSource -> 102 - returnRepliesWithModel 103 - { model | step = Where, context = defaultContext } 104 - [ ReplaceSourceInCollection model.context 105 - , ProcessSources [ model.context ] 106 - , GoToPage (Page.Sources Sources.Index) 107 - ] 108 - 109 - RenameSource -> 110 - returnRepliesWithModel 111 - { model | step = Where, context = defaultContext } 112 - [ ReplaceSourceInCollection model.context 113 - , GoToPage (Page.Sources Sources.Index) 114 - ] 115 - 116 - ReturnToIndex -> 117 - returnRepliesWithModel 118 - model 119 - [ GoToPage (Page.Sources Sources.Index) ] 120 - 121 - SelectService serviceKey -> 122 - case Services.keyToType serviceKey of 123 - Just service -> 124 - let 125 - ( context, data ) = 126 - ( model.context 127 - , Services.initialData service 128 - ) 129 - 130 - newContext = 131 - { context | data = data, service = service } 132 - in 133 - return { model | context = newContext } 134 - 135 - Nothing -> 136 - return model 137 - 138 - SetData key value -> 139 - let 140 - context = 141 - model.context 142 - 143 - updatedData = 144 - Dict.insert key value context.data 145 - 146 - newContext = 147 - { context | data = updatedData } 148 - in 149 - return { model | context = newContext } 150 - 151 - TakeStep -> 152 - case ( model.step, model.context.service ) of 153 - ( How, Dropbox ) -> 154 - model.context.data 155 - |> Sources.Services.Dropbox.authorizationUrl 156 - |> ExternalSourceAuthorization 157 - |> returnReplyWithModel model 158 - 159 - ( How, Google ) -> 160 - model.context.data 161 - |> Sources.Services.Google.authorizationUrl 162 - |> ExternalSourceAuthorization 163 - |> returnReplyWithModel model 164 - 165 - _ -> 166 - return { model | step = takeStepForwards model.step } 167 - 168 - TakeStepBackwards -> 169 - return { model | step = takeStepBackwards model.step } 170 - 171 - 172 - takeStepForwards : FormStep -> FormStep 173 - takeStepForwards currentStep = 174 - case currentStep of 175 - Where -> 176 - How 177 - 178 - _ -> 179 - By 180 - 181 - 182 - takeStepBackwards : FormStep -> FormStep 183 - takeStepBackwards currentStep = 184 - case currentStep of 185 - By -> 186 - How 187 - 188 - _ -> 189 - Where 190 - 191 - 192 - 193 56 -- NEW 194 57 195 58 ··· 197 60 { onboarding : Bool } 198 61 199 62 200 - new : Arguments -> Model -> List (Html Msg) 63 + new : Arguments -> Form -> List (Html Msg) 201 64 new args model = 202 65 case model.step of 203 66 Where -> ··· 210 73 newBy model 211 74 212 75 213 - newWhere : Arguments -> Model -> List (Html Msg) 76 + newWhere : Arguments -> Form -> List (Html Msg) 214 77 newWhere { onboarding } { context } = 215 78 [ ----------------------------------------- 216 79 -- Navigation ··· 264 127 ] 265 128 266 129 267 - newHow : Model -> List (Html Msg) 130 + newHow : Form -> List (Html Msg) 268 131 newHow { context } = 269 132 [ ----------------------------------------- 270 133 -- Navigation ··· 338 201 ] 339 202 340 203 341 - newBy : Model -> List (Html Msg) 204 + newBy : Form -> List (Html Msg) 342 205 newBy { context } = 343 206 [ ----------------------------------------- 344 207 -- Navigation ··· 354 217 -- Content 355 218 ----------------------------------------- 356 219 , (\h -> 357 - form AddSource 220 + form AddSourceUsingForm 358 221 [ UI.Kit.canisterForm h ] 359 222 ) 360 223 [ UI.Kit.h2 "One last thing" ··· 376 239 ] 377 240 [ UI.Kit.textField 378 241 [ name "name" 379 - , onInput (SetData "name") 242 + , onInput (SetFormData "name") 380 243 , value nameValue 381 244 ] 382 245 ] ··· 442 305 -- EDIT 443 306 444 307 445 - edit : Model -> List (Html Msg) 308 + edit : Form -> List (Html Msg) 446 309 edit { context } = 447 310 [ ----------------------------------------- 448 311 -- Navigation ··· 458 321 -- Content 459 322 ----------------------------------------- 460 323 , (\h -> 461 - form EditSource 324 + form EditSourceUsingForm 462 325 [ chunk 463 326 [ C.text_left, C.w_full ] 464 327 [ UI.Kit.canister h ] ··· 536 399 bool 537 400 |> not 538 401 |> boolToString 539 - |> SetData property.key 402 + |> SetFormData property.key 540 403 } 541 404 ] 542 405 543 406 else 544 407 UI.Kit.textField 545 408 [ name property.key 546 - , onInput (SetData property.key) 409 + , onInput (SetFormData property.key) 547 410 , placeholder property.placeholder 548 411 , required (property.label |> String.toLower |> String.contains "optional" |> not) 549 412 , type_ (ifThenElse property.password "password" "text") ··· 658 521 -- RENAME 659 522 660 523 661 - rename : Model -> List (Html Msg) 524 + rename : Form -> List (Html Msg) 662 525 rename { context } = 663 526 [ ----------------------------------------- 664 527 -- Navigation ··· 674 537 -- Content 675 538 ----------------------------------------- 676 539 , (\h -> 677 - form RenameSource 540 + form RenameSourceUsingForm 678 541 [ UI.Kit.canisterForm h ] 679 542 ) 680 543 [ UI.Kit.h2 "Name your source" ··· 682 545 -- Input 683 546 -------- 684 547 , [ name "name" 685 - , onInput (SetData "name") 548 + , onInput (SetFormData "name") 686 549 , value (Dict.fetch "name" "" context.data) 687 550 ] 688 551 |> UI.Kit.textField
+505 -7
src/Applications/UI/Sources/State.elm
··· 1 1 module UI.Sources.State exposing (..) 2 2 3 + import Alien 4 + import Common 5 + import Conditional exposing (ifThenElse) 6 + import Coordinates 7 + import Dict 8 + import Dict.Ext as Dict 9 + import Html.Events.Extra.Mouse as Mouse 10 + import Json.Decode as Json 11 + import Json.Encode 3 12 import Monocle.Lens as Lens exposing (Lens) 13 + import Notifications 14 + import Return exposing (andThen, return) 15 + import Return.Ext as Return 16 + import Sources exposing (..) 17 + import Sources.Encoding as Sources 18 + import Sources.Services as Services 19 + import Sources.Services.Dropbox 20 + import Sources.Services.Google 21 + import UI.Common.State as Common 22 + import UI.Page as Page 23 + import UI.Ports as Ports 24 + import UI.Reply as Reply 25 + import UI.Sources.ContextMenu as Sources 26 + import UI.Sources.Form as Form 27 + import UI.Sources.Page as Sources 28 + import UI.Sources.Types exposing (..) 29 + import UI.Types as UI exposing (Manager, Model) 30 + import UI.User.State.Export as User 4 31 5 32 6 33 7 34 -- 🌳 8 35 9 36 10 - lens = 11 - { get = .sources 12 - , set = \sources m -> { m | sources = sources } 37 + formLens = 38 + { get = .sourceForm 39 + , set = \form m -> { m | sourceForm = form } 13 40 } 14 41 15 42 16 - formLens = 43 + formContextLens = 17 44 Lens.compose 18 - lens 19 - { get = .form 20 - , set = \form m -> { m | form = form } 45 + formLens 46 + { get = .context 47 + , set = \context m -> { m | context = context } 21 48 } 49 + 50 + 51 + formStepLens = 52 + Lens.compose 53 + formLens 54 + { get = .step 55 + , set = \step m -> { m | step = step } 56 + } 57 + 58 + 59 + 60 + -- 📣 61 + 62 + 63 + update : Msg -> Manager 64 + update msg = 65 + case msg of 66 + Bypass -> 67 + Return.singleton 68 + 69 + -- 70 + FinishedProcessingSource a -> 71 + finishedProcessingSource a 72 + 73 + FinishedProcessing -> 74 + finishedProcessing 75 + 76 + Process -> 77 + process 78 + 79 + ReportProcessingError a -> 80 + reportProcessingError a 81 + 82 + ReportProcessingProgress a -> 83 + reportProcessingProgress a 84 + 85 + StopProcessing -> 86 + stopProcessing 87 + 88 + ----------------------------------------- 89 + -- Collection 90 + ----------------------------------------- 91 + AddToCollection a -> 92 + addToCollection a 93 + 94 + RemoveFromCollection a -> 95 + removeFromCollection a 96 + 97 + UpdateSourceData a -> 98 + updateSourceData a 99 + 100 + ----------------------------------------- 101 + -- Form 102 + ----------------------------------------- 103 + AddSourceUsingForm -> 104 + addSourceUsingForm 105 + 106 + EditSourceUsingForm -> 107 + editSourceUsingForm 108 + 109 + RenameSourceUsingForm -> 110 + renameSourceUsingForm 111 + 112 + ReturnToIndex -> 113 + returnToIndex 114 + 115 + SelectService a -> 116 + selectService a 117 + 118 + SetFormData a b -> 119 + setFormData a b 120 + 121 + TakeStep -> 122 + takeStep 123 + 124 + TakeStepBackwards -> 125 + takeStepBackwards 126 + 127 + ----------------------------------------- 128 + -- Individual 129 + ----------------------------------------- 130 + SourceContextMenu a b -> 131 + sourceContextMenu a b 132 + 133 + ToggleActivation a -> 134 + toggleActivation a 135 + 136 + ToggleDirectoryPlaylists a -> 137 + toggleDirectoryPlaylists a 138 + 139 + 140 + 141 + -- 🛠 142 + 143 + 144 + finishedProcessing : Manager 145 + finishedProcessing model = 146 + (case model.processingNotificationId of 147 + Just id -> 148 + Common.dismissNotification { id = id } 149 + 150 + Nothing -> 151 + Return.singleton 152 + ) 153 + { model | processingContext = [] } 154 + 155 + 156 + finishedProcessingSource : { sourceId : String } -> Manager 157 + finishedProcessingSource { sourceId } model = 158 + model.processingContext 159 + |> List.filter (Tuple.first >> (/=) sourceId) 160 + |> (\newContext -> { model | processingContext = newContext }) 161 + |> Return.singleton 162 + 163 + 164 + process : Manager 165 + process model = 166 + case sourcesToProcess model of 167 + [] -> 168 + Return.singleton model 169 + 170 + toProcess -> 171 + let 172 + notification = 173 + Notifications.stickyWarning "Processing sources ..." 174 + 175 + notificationId = 176 + Notifications.id notification 177 + 178 + newNotifications = 179 + List.filter 180 + (\n -> Notifications.kind n /= Notifications.Error) 181 + model.notifications 182 + 183 + processingContext = 184 + toProcess 185 + |> List.sortBy (.data >> Dict.fetch "name" "") 186 + |> List.map (\{ id } -> ( id, 0 )) 187 + 188 + newModel = 189 + { model 190 + | notifications = newNotifications 191 + , processingContext = processingContext 192 + , processingError = Nothing 193 + , processingNotificationId = Just notificationId 194 + } 195 + in 196 + [ ( "origin" 197 + , Json.Encode.string (Common.urlOrigin model.url) 198 + ) 199 + , ( "sources" 200 + , Json.Encode.list Sources.encode toProcess 201 + ) 202 + ] 203 + |> Json.Encode.object 204 + |> Alien.broadcast Alien.ProcessSources 205 + |> Ports.toBrain 206 + |> return newModel 207 + |> andThen (Common.showNotification notification) 208 + 209 + 210 + reportProcessingError : Json.Value -> Manager 211 + reportProcessingError json model = 212 + case Json.decodeValue (Json.dict Json.string) json of 213 + Ok dict -> 214 + let 215 + args = 216 + { error = Dict.fetch "error" "" dict 217 + , sourceId = Dict.fetch "sourceId" "" dict 218 + } 219 + in 220 + [] 221 + |> Notifications.errorWithCode 222 + ("Could not process the _" 223 + ++ Dict.fetch "sourceName" "" dict 224 + ++ "_ source. I got the following response from the source:" 225 + ) 226 + (Dict.fetch "error" "missingError" dict) 227 + |> Common.showNotificationWithModel 228 + { model | processingError = Just args } 229 + 230 + Err _ -> 231 + "Could not decode processing error" 232 + |> Notifications.stickyError 233 + |> Common.showNotificationWithModel model 234 + 235 + 236 + reportProcessingProgress : Json.Value -> Manager 237 + reportProcessingProgress json model = 238 + case 239 + Json.decodeValue 240 + (Json.map2 241 + (\p s -> 242 + { progress = p 243 + , sourceId = s 244 + } 245 + ) 246 + (Json.field "progress" Json.float) 247 + (Json.field "sourceId" Json.string) 248 + ) 249 + json 250 + of 251 + Ok { progress, sourceId } -> 252 + model.processingContext 253 + |> List.map 254 + (\( sid, pro ) -> 255 + ifThenElse (sid == sourceId) 256 + ( sid, progress ) 257 + ( sid, pro ) 258 + ) 259 + |> (\processingContext -> 260 + { model | processingContext = processingContext } 261 + ) 262 + |> Return.singleton 263 + 264 + Err _ -> 265 + "Could not decode processing progress" 266 + |> Notifications.stickyError 267 + |> Common.showNotificationWithModel model 268 + 269 + 270 + stopProcessing : Manager 271 + stopProcessing model = 272 + case model.processingNotificationId of 273 + Just notificationId -> 274 + Alien.StopProcessing 275 + |> Alien.trigger 276 + |> Ports.toBrain 277 + |> return 278 + { model 279 + | processingContext = [] 280 + , processingNotificationId = Nothing 281 + } 282 + |> andThen (Common.dismissNotification { id = notificationId }) 283 + 284 + Nothing -> 285 + Return.singleton model 286 + 287 + 288 + 289 + -- COLLECTION 290 + 291 + 292 + addToCollection : Source -> Manager 293 + addToCollection unsuitableSource model = 294 + let 295 + source = 296 + setProperId 297 + (List.length model.sources + 1) 298 + model.currentTime 299 + unsuitableSource 300 + in 301 + { model | sources = model.sources ++ [ source ] } 302 + |> Return.performance (UI.Reply Reply.SaveSources) 303 + |> andThen process 304 + 305 + 306 + removeFromCollection : { sourceId : String } -> Manager 307 + removeFromCollection { sourceId } model = 308 + model.sources 309 + |> List.filter (.id >> (/=) sourceId) 310 + |> (\c -> { model | sources = c }) 311 + |> Return.singleton 312 + |> andThen (Return.performance <| UI.Reply Reply.SaveSources) 313 + |> andThen (Return.performance <| UI.Reply <| Reply.RemoveTracksWithSourceId sourceId) 314 + 315 + 316 + updateSourceData : Json.Value -> Manager 317 + updateSourceData json model = 318 + json 319 + |> Sources.decode 320 + |> Maybe.map 321 + (\source -> 322 + List.map 323 + (\s -> 324 + if s.id == source.id then 325 + source 326 + 327 + else 328 + s 329 + ) 330 + model.sources 331 + ) 332 + |> Maybe.map (\col -> { model | sources = col }) 333 + |> Maybe.withDefault model 334 + |> Return.performance (UI.Reply Reply.SaveSources) 335 + 336 + 337 + 338 + -- FORM 339 + 340 + 341 + addSourceUsingForm : Manager 342 + addSourceUsingForm model = 343 + let 344 + context = 345 + model.sourceForm.context 346 + 347 + cleanContext = 348 + { context | data = Dict.map (always String.trim) context.data } 349 + in 350 + model 351 + |> formLens.set Form.initialModel 352 + |> addToCollection cleanContext 353 + |> andThen returnToIndex 354 + 355 + 356 + editSourceUsingForm : Manager 357 + editSourceUsingForm model = 358 + model 359 + |> formLens.set Form.initialModel 360 + |> replaceSourceInCollection model.sourceForm.context 361 + |> andThen process 362 + |> andThen returnToIndex 363 + 364 + 365 + renameSourceUsingForm : Manager 366 + renameSourceUsingForm model = 367 + model 368 + |> formLens.set Form.initialModel 369 + |> replaceSourceInCollection model.sourceForm.context 370 + |> andThen returnToIndex 371 + 372 + 373 + returnToIndex : Manager 374 + returnToIndex = 375 + Common.changeUrlUsingPage (Page.Sources Sources.Index) 376 + 377 + 378 + selectService : String -> Manager 379 + selectService serviceKey model = 380 + case Services.keyToType serviceKey of 381 + Just service -> 382 + model 383 + |> Lens.modify 384 + formContextLens 385 + (\c -> 386 + { c 387 + | data = Services.initialData service 388 + , service = service 389 + } 390 + ) 391 + |> Return.singleton 392 + 393 + Nothing -> 394 + Return.singleton model 395 + 396 + 397 + setFormData : String -> String -> Manager 398 + setFormData key value model = 399 + model 400 + |> Lens.modify 401 + formContextLens 402 + (\context -> 403 + context.data 404 + |> Dict.insert key value 405 + |> (\data -> { context | data = data }) 406 + ) 407 + |> Return.singleton 408 + 409 + 410 + takeStep : Manager 411 + takeStep model = 412 + let 413 + form = 414 + formLens.get model 415 + in 416 + case ( form.step, form.context.service ) of 417 + ( How, Dropbox ) -> 418 + form.context.data 419 + |> Sources.Services.Dropbox.authorizationUrl 420 + |> Reply.ExternalSourceAuthorization 421 + |> UI.Reply 422 + |> Return.performanceF model 423 + 424 + ( How, Google ) -> 425 + form.context.data 426 + |> Sources.Services.Google.authorizationUrl 427 + |> Reply.ExternalSourceAuthorization 428 + |> UI.Reply 429 + |> Return.performanceF model 430 + 431 + _ -> 432 + model 433 + |> Lens.modify formStepLens takeStepForwards 434 + |> Return.singleton 435 + 436 + 437 + takeStepBackwards : Manager 438 + takeStepBackwards = 439 + Lens.modify formStepLens takeStepBackwards_ >> Return.singleton 440 + 441 + 442 + 443 + -- INDIVIDUAL 444 + 445 + 446 + sourceContextMenu : Source -> Mouse.Event -> Manager 447 + sourceContextMenu source mouseEvent model = 448 + mouseEvent.clientPos 449 + |> Coordinates.fromTuple 450 + |> Sources.sourceMenu source 451 + |> Common.showContextMenuWithModel model 452 + 453 + 454 + toggleActivation : { sourceId : String } -> Manager 455 + toggleActivation { sourceId } model = 456 + model.sources 457 + |> List.map 458 + (\source -> 459 + if source.id == sourceId then 460 + { source | enabled = not source.enabled } 461 + 462 + else 463 + source 464 + ) 465 + |> (\collection -> { model | sources = collection }) 466 + |> Return.performance (UI.Reply Reply.SaveSources) 467 + 468 + 469 + toggleDirectoryPlaylists : { sourceId : String } -> Manager 470 + toggleDirectoryPlaylists { sourceId } model = 471 + model.sources 472 + |> List.map 473 + (\source -> 474 + if source.id == sourceId then 475 + { source | directoryPlaylists = not source.directoryPlaylists } 476 + 477 + else 478 + source 479 + ) 480 + |> (\collection -> { model | sources = collection }) 481 + |> Return.performance (UI.Reply Reply.SaveSources) 482 + |> andThen (Return.performance <| UI.Reply Reply.GenerateDirectoryPlaylists) 483 + 484 + 485 + 486 + -- ⚗️ 487 + 488 + 489 + replaceSourceInCollection : Source -> Manager 490 + replaceSourceInCollection source model = 491 + model.sources 492 + |> List.map (\s -> ifThenElse (s.id == source.id) source s) 493 + |> (\s -> { model | sources = s }) 494 + |> Return.performance (UI.Reply Reply.SaveSources) 495 + 496 + 497 + sourcesToProcess : Model -> List Source 498 + sourcesToProcess model = 499 + List.filter (.enabled >> (==) True) model.sources 500 + 501 + 502 + takeStepForwards : FormStep -> FormStep 503 + takeStepForwards currentStep = 504 + case currentStep of 505 + Where -> 506 + How 507 + 508 + _ -> 509 + By 510 + 511 + 512 + takeStepBackwards_ : FormStep -> FormStep 513 + takeStepBackwards_ currentStep = 514 + case currentStep of 515 + By -> 516 + How 517 + 518 + _ -> 519 + Where
+59
src/Applications/UI/Sources/Types.elm
··· 1 + module UI.Sources.Types exposing (..) 2 + 3 + import Html.Events.Extra.Mouse as Mouse 4 + import Json.Decode as Json 5 + import Sources exposing (..) 6 + 7 + 8 + 9 + -- 🌳 10 + 11 + 12 + type alias Form = 13 + { context : Source 14 + , step : FormStep 15 + } 16 + 17 + 18 + type FormStep 19 + = Where 20 + | How 21 + | By 22 + 23 + 24 + 25 + -- 📣 26 + 27 + 28 + type Msg 29 + = Bypass 30 + -- 31 + | FinishedProcessingSource { sourceId : String } 32 + | FinishedProcessing 33 + | Process 34 + | ReportProcessingError Json.Value 35 + | ReportProcessingProgress Json.Value 36 + | StopProcessing 37 + ----------------------------------------- 38 + -- Collection 39 + ----------------------------------------- 40 + | AddToCollection Source 41 + | RemoveFromCollection { sourceId : String } 42 + | UpdateSourceData Json.Value 43 + ----------------------------------------- 44 + -- Form 45 + ----------------------------------------- 46 + | AddSourceUsingForm 47 + | EditSourceUsingForm 48 + | RenameSourceUsingForm 49 + | ReturnToIndex 50 + | SelectService String 51 + | SetFormData String String 52 + | TakeStep 53 + | TakeStepBackwards 54 + ----------------------------------------- 55 + -- Individual 56 + ----------------------------------------- 57 + | SourceContextMenu Source Mouse.Event 58 + | ToggleActivation { sourceId : String } 59 + | ToggleDirectoryPlaylists { sourceId : String }
+293
src/Applications/UI/Sources/View.elm
··· 1 + module UI.Sources.View exposing (view) 2 + 3 + import Alien 4 + import Chunky exposing (..) 5 + import Conditional exposing (ifThenElse) 6 + import Coordinates 7 + import Css.Classes as C 8 + import Dict.Ext as Dict 9 + import Html exposing (Html, text) 10 + import Html.Attributes exposing (href) 11 + import Html.Events.Extra.Mouse as Mouse 12 + import Html.Lazy as Lazy 13 + import Json.Decode as Json 14 + import List.Extra as List 15 + import Material.Icons as Icons 16 + import Material.Icons.Types exposing (Coloring(..)) 17 + import Return3 as Return exposing (..) 18 + import Sources exposing (..) 19 + import Sources.Encoding 20 + import Time 21 + import Time.Ext as Time 22 + import UI.Kit exposing (ButtonType(..)) 23 + import UI.List 24 + import UI.Navigation exposing (..) 25 + import UI.Page as Page 26 + import UI.Ports as Ports 27 + import UI.Reply exposing (Reply(..)) 28 + import UI.Sources.Form as Form 29 + import UI.Sources.Page as Sources exposing (..) 30 + import UI.Sources.Types exposing (Msg(..)) 31 + import UI.Types as UI exposing (Model) 32 + 33 + 34 + 35 + -- 🌳 36 + 37 + 38 + type alias PartialModel = 39 + { processingContext : List ( String, Float ) 40 + , processingError : Maybe { error : String, sourceId : String } 41 + , sources : List Source 42 + } 43 + 44 + 45 + 46 + -- 🗺 47 + 48 + 49 + view : Sources.Page -> Model -> Html UI.Msg 50 + view page model = 51 + Html.map UI.SourcesMsg 52 + (case page of 53 + Index -> 54 + Lazy.lazy2 55 + (\a b -> receptacle <| index a b) 56 + (List.length model.tracks.collection.untouched) 57 + { processingContext = model.processingContext 58 + , processingError = model.processingError 59 + , sources = model.sources 60 + } 61 + 62 + Edit sourceId -> 63 + lazyForm model <| Form.edit 64 + 65 + New -> 66 + lazyForm model <| Form.new { onboarding = False } 67 + 68 + NewOnboarding -> 69 + lazyForm model <| Form.new { onboarding = True } 70 + 71 + NewThroughRedirect _ _ -> 72 + lazyForm model <| Form.new { onboarding = False } 73 + 74 + Rename sourceId -> 75 + lazyForm model <| Form.rename 76 + ) 77 + 78 + 79 + lazyForm model formView = 80 + Lazy.lazy (formView >> receptacle) model.sourceForm 81 + 82 + 83 + receptacle = 84 + UI.Kit.receptacle { scrolling = True } 85 + 86 + 87 + 88 + -- INDEX 89 + 90 + 91 + index : Int -> PartialModel -> List (Html Msg) 92 + index amountOfTracks model = 93 + [ ----------------------------------------- 94 + -- Navigation 95 + ----------------------------------------- 96 + if List.isEmpty model.sources then 97 + UI.Navigation.local 98 + [ ( Icon Icons.add 99 + , Label "Add a new source" Shown 100 + , NavigateToPage (Page.Sources New) 101 + ) 102 + ] 103 + 104 + else 105 + UI.Navigation.local 106 + [ ( Icon Icons.add 107 + , Label "Add a new source" Shown 108 + , NavigateToPage (Page.Sources New) 109 + ) 110 + 111 + -- Process 112 + ---------- 113 + , if List.isEmpty model.processingContext then 114 + ( Icon Icons.sync 115 + , Label "Process sources" Shown 116 + , PerformMsg Process 117 + ) 118 + 119 + else 120 + ( Icon Icons.sync 121 + , Label "Stop processing ..." Shown 122 + , PerformMsg StopProcessing 123 + ) 124 + ] 125 + 126 + ----------------------------------------- 127 + -- Content 128 + ----------------------------------------- 129 + , if List.isEmpty model.sources then 130 + chunk 131 + [ C.relative ] 132 + [ chunk 133 + [ C.absolute, C.left_0, C.top_0 ] 134 + [ UI.Kit.canister [ UI.Kit.h1 "Sources" ] ] 135 + ] 136 + 137 + else 138 + UI.Kit.canister 139 + [ UI.Kit.h1 "Sources" 140 + 141 + -- Intro 142 + -------- 143 + , intro amountOfTracks 144 + 145 + -- List 146 + ------- 147 + , model.sources 148 + |> List.sortBy 149 + (.data >> Dict.fetch "name" "") 150 + |> List.map 151 + (\source -> 152 + { label = Html.text (Dict.fetch "name" "" source.data) 153 + , actions = sourceActions model.processingContext model.processingError source 154 + , msg = Nothing 155 + , isSelected = False 156 + } 157 + ) 158 + |> UI.List.view UI.List.Normal 159 + ] 160 + 161 + -- 162 + , if List.isEmpty model.sources then 163 + UI.Kit.centeredContent 164 + [ slab 165 + Html.a 166 + [ href (Page.toString <| Page.Sources New) ] 167 + [ C.block 168 + , C.opacity_30 169 + , C.text_inherit 170 + ] 171 + [ Icons.music_note 64 Inherit ] 172 + , slab 173 + Html.a 174 + [ href (Page.toString <| Page.Sources New) ] 175 + [ C.block 176 + , C.leading_normal 177 + , C.mt_2 178 + , C.opacity_40 179 + , C.text_center 180 + , C.text_inherit 181 + ] 182 + [ text "A source is a place where music is stored," 183 + , lineBreak 184 + , text "add one so you can play some music " 185 + , inline 186 + [ C.align_middle, C.inline_block, C.minus_mt_px ] 187 + [ Icons.add 14 Inherit ] 188 + ] 189 + ] 190 + 191 + else 192 + nothing 193 + ] 194 + 195 + 196 + intro : Int -> Html Msg 197 + intro amountOfTracks = 198 + [ text "A source is a place where your music is stored." 199 + , lineBreak 200 + , text "By connecting a source, the application will scan it and keep a list of all the music in it." 201 + , lineBreak 202 + , text "You currently have " 203 + , text (String.fromInt amountOfTracks) 204 + , text " " 205 + , text (ifThenElse (amountOfTracks == 1) "track" "tracks") 206 + , text " in your collection." 207 + ] 208 + |> raw 209 + |> UI.Kit.intro 210 + 211 + 212 + sourceActions : List ( String, Float ) -> Maybe { error : String, sourceId : String } -> Source -> List (UI.List.Action Msg) 213 + sourceActions processingContext processingError source = 214 + let 215 + processIndex = 216 + List.findIndex (Tuple.first >> (==) source.id) processingContext 217 + 218 + process = 219 + Maybe.andThen (\idx -> List.getAt idx processingContext) processIndex 220 + in 221 + List.append 222 + (case ( process, processingError ) of 223 + ( Just ( _, progress ), _ ) -> 224 + [ { icon = 225 + \_ _ -> 226 + if progress < 0.05 then 227 + inline 228 + [ C.inline_block, C.opacity_70, C.px_1 ] 229 + [ case processIndex of 230 + Just 0 -> 231 + Html.text "Listing" 232 + 233 + _ -> 234 + Html.text "Waiting" 235 + ] 236 + 237 + else 238 + progress 239 + |> (*) 100 240 + |> round 241 + |> String.fromInt 242 + |> (\s -> s ++ "%") 243 + |> Html.text 244 + |> List.singleton 245 + |> inline [ C.inline_block, C.opacity_70, C.px_1 ] 246 + , msg = Nothing 247 + , title = "" 248 + } 249 + , { icon = Icons.sync 250 + , msg = Nothing 251 + , title = "Currently processing" 252 + } 253 + ] 254 + 255 + ( Nothing, Just { error, sourceId } ) -> 256 + if sourceId == source.id then 257 + [ { icon = \size _ -> Icons.error_outline size (Color UI.Kit.colors.error) 258 + , msg = Nothing 259 + , title = error 260 + } 261 + ] 262 + 263 + else 264 + [] 265 + 266 + _ -> 267 + [] 268 + ) 269 + [ { icon = 270 + if source.enabled then 271 + Icons.check 272 + 273 + else 274 + Icons.block 275 + , msg = 276 + { sourceId = source.id } 277 + |> ToggleActivation 278 + |> always 279 + |> Just 280 + , title = 281 + if source.enabled then 282 + "Enabled (click to disable)" 283 + 284 + else 285 + "Disabled (click to enable)" 286 + } 287 + 288 + -- 289 + , { icon = Icons.more_vert 290 + , msg = Just (SourceContextMenu source) 291 + , title = "Menu" 292 + } 293 + ]
+2 -2
src/Applications/UI/Tracks.elm
··· 867 867 868 868 869 869 noTracksView : List String -> Int -> Int -> Int -> Html Msg 870 - noTracksView isProcessing amountOfSources amountOfTracks amountOfFavourites = 870 + noTracksView processingContext amountOfSources amountOfTracks amountOfFavourites = 871 871 chunk 872 872 [ C.flex, C.flex_grow ] 873 873 [ UI.Kit.centeredContent 874 - [ if List.length isProcessing > 0 then 874 + [ if List.length processingContext > 0 then 875 875 message "Processing Tracks" 876 876 877 877 else if amountOfSources == 0 then
+17 -12
src/Applications/UI/Types.elm
··· 24 24 import Notifications exposing (Notification) 25 25 import Playlists exposing (Playlist, PlaylistTrack) 26 26 import Queue 27 - import Sources 27 + import Sources exposing (Source) 28 28 import Sources.Encoding as Sources 29 29 import Time 30 30 import Tracks ··· 35 35 import UI.Page as Page exposing (Page) 36 36 import UI.Queue.Types as Queue 37 37 import UI.Reply as Reply exposing (Reply(..)) 38 - import UI.Sources as Sources 39 38 import UI.Sources.ContextMenu as Sources 39 + import UI.Sources.Types as Sources 40 40 import UI.Tracks as Tracks 41 41 import UI.Tracks.ContextMenu as Tracks 42 42 import Url exposing (Protocol(..), Url) ··· 141 141 , shuffle : Bool 142 142 143 143 ----------------------------------------- 144 + -- Sources 145 + ----------------------------------------- 146 + , processingContext : List ( String, Float ) 147 + , processingError : Maybe { error : String, sourceId : String } 148 + , processingNotificationId : Maybe Int 149 + , sourceForm : Sources.Form 150 + , sources : List Source 151 + 152 + ----------------------------------------- 144 153 -- 🦉 Nested 145 154 ----------------------------------------- 146 155 , authentication : Authentication.State ··· 148 157 ----------------------------------------- 149 158 -- Children (TODO) 150 159 ----------------------------------------- 151 - , sources : Sources.Model 152 160 , tracks : Tracks.Model 153 161 } 154 162 ··· 238 246 | GotLastFmSession (Result Http.Error String) 239 247 | Scrobble { duration : Int, timestamp : Int, trackId : String } 240 248 ----------------------------------------- 241 - -- Tracks 249 + -- Tracks (TODO: Move to Tracks.Types) 242 250 ----------------------------------------- 243 251 | DownloadTracksFinished 244 252 | FailedToStoreTracksInCache (List String) ··· 256 264 ----------------------------------------- 257 265 | KeyboardMsg Keyboard.Msg 258 266 ----------------------------------------- 259 - -- 📭 Other 260 - ----------------------------------------- 261 - | SetCurrentTime Time.Posix 262 - | SetIsOnline Bool 263 - ----------------------------------------- 264 267 -- 🦉 Nested 265 268 ----------------------------------------- 266 269 | AuthenticationMsg Authentication.Msg 267 270 | QueueMsg Queue.Msg 271 + | SourcesMsg Sources.Msg 272 + | TracksMsg Tracks.Msg 268 273 ----------------------------------------- 269 - -- Children (TODO) 274 + -- 📭 Other 270 275 ----------------------------------------- 271 - | SourcesMsg Sources.Msg 272 - | TracksMsg Tracks.Msg 276 + | SetCurrentTime Time.Posix 277 + | SetIsOnline Bool 273 278 274 279 275 280 type alias Organizer model =
+5 -12
src/Applications/UI/User/State/Import.elm
··· 20 20 import UI.Ports as Ports 21 21 import UI.Reply exposing (..) 22 22 import UI.Reply.Translate as Reply 23 - import UI.Routing.State as Routing 24 - import UI.Sources as Sources 23 + import UI.Sources.State as Sources 25 24 import UI.Tracks as Tracks 26 25 import UI.Types as UI exposing (..) 27 26 import Url.Ext as Url ··· 58 57 -- Clear tracks cache 59 58 |> andThen (Reply.translate ClearTracksCache) 60 59 -- Redirect to index page 61 - |> andThen (Routing.changeUrlUsingPage Page.Index) 60 + |> andThen (Common.changeUrlUsingPage Page.Index) 62 61 ----------------------------- 63 62 -- Save all the imported data 64 63 ----------------------------- ··· 107 106 |> andThen 108 107 (\m -> 109 108 if m.processAutomatically then 110 - m.sources 111 - |> Sources.sourcesToProcess 112 - |> ProcessSources 113 - |> Reply.translateWithModel m 109 + Sources.process m 114 110 115 111 else 116 112 Return.singleton m ··· 134 130 |> Maybe.andThen .backgroundImage 135 131 |> Maybe.withDefault Backdrop.default 136 132 |> Just 137 - 138 - sourcesModel = 139 - { sources | collection = data.sources } 140 133 141 134 newPlaylistsCollection = 142 135 List.append ··· 155 148 model.lastFm 156 149 in 157 150 ( { model 158 - | sources = sourcesModel 159 - , tracks = tracksModel 151 + | tracks = tracksModel 160 152 161 153 -- 162 154 , chosenBackdrop = chosenBackdrop ··· 166 158 , processAutomatically = Maybe.unwrap True .processAutomatically data.settings 167 159 , progress = data.progress 168 160 , rememberProgress = Maybe.unwrap True .rememberProgress data.settings 161 + , sources = data.sources 169 162 } 170 163 -- 171 164 , Cmd.map TracksMsg tracksCmd
+4 -10
src/Applications/UI/View.elm
··· 41 41 import UI.Reply exposing (Reply(..)) 42 42 import UI.Settings as Settings 43 43 import UI.Settings.Page 44 - import UI.Sources as Sources 45 44 import UI.Sources.ContextMenu as Sources 46 45 import UI.Sources.Page 46 + import UI.Sources.View as Sources 47 47 import UI.Svg.Elements 48 48 import UI.Tracks as Tracks 49 49 import UI.Tracks.ContextMenu as Tracks ··· 158 158 -- Main 159 159 ----------------------------------------- 160 160 , vessel 161 - [ { amountOfSources = List.length model.sources.collection 161 + [ { amountOfSources = List.length model.sources 162 162 , bgColor = model.extractedBackdropColor 163 163 , darkMode = model.darkMode 164 164 , isOnIndexPage = model.page == Page.Index 165 165 , isTouchDevice = model.isTouchDevice 166 - , sourceIdsBeingProcessed = List.map Tuple.first model.sources.isProcessing 166 + , sourceIdsBeingProcessed = List.map Tuple.first model.processingContext 167 167 , viewport = model.viewport 168 168 } 169 169 |> Tracks.view model.tracks ··· 202 202 |> Html.map Reply 203 203 204 204 Page.Sources subPage -> 205 - let 206 - amountOfTracks = 207 - List.length model.tracks.collection.untouched 208 - in 209 - model.sources 210 - |> Lazy.lazy3 Sources.view { amountOfTracks = amountOfTracks } subPage 211 - |> Html.map SourcesMsg 205 + Sources.view subPage model 212 206 ] 213 207 214 208 -----------------------------------------
+9
src/Library/Lens/Ext.elm
··· 3 3 import Monocle.Lens as Lens exposing (Lens) 4 4 5 5 6 + {-| Flipped version of `Lens.modify`. 7 + -} 6 8 adjust : Lens a b -> a -> (b -> b) -> a 7 9 adjust lens a fn = 8 10 Lens.modify lens fn a 11 + 12 + 13 + {-| Flipped version of `lens.set`. 14 + -} 15 + replace : Lens a b -> a -> b -> a 16 + replace lens a b = 17 + lens.set b a
+2 -1
src/Library/Sources/Encoding.elm
··· 39 39 40 40 decode : Decode.Value -> Maybe Source 41 41 decode value = 42 - Decode.decodeValue decoder value 42 + value 43 + |> Decode.decodeValue decoder 43 44 |> Result.toMaybe 44 45 45 46