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

+656 -654
+33 -14
src/Applications/UI.elm
··· 39 39 import UI.EtCetera.State as EtCetera 40 40 import UI.Interface.State as Interface 41 41 import UI.Page as Page 42 - import UI.Playlists as Playlists 43 42 import UI.Playlists.ContextMenu as Playlists 44 43 import UI.Playlists.State as Playlists 45 44 import UI.Ports as Ports ··· 153 152 , notifications = [] 154 153 155 154 ----------------------------------------- 155 + -- Playlists 156 + ----------------------------------------- 157 + , editPlaylistContext = Nothing 158 + , lastModifiedPlaylist = Nothing 159 + , newPlaylistContext = Nothing 160 + , playlists = [] 161 + , playlistToActivate = Nothing 162 + 163 + ----------------------------------------- 156 164 -- Children (TODO) 157 165 ----------------------------------------- 158 166 , authentication = Authentication.initialModel url 159 - , playlists = Playlists.initialModel 160 167 , queue = Queue.initialModel 161 168 , sources = Sources.initialModel 162 169 , tracks = Tracks.initialModel ··· 308 315 ----------------------------------------- 309 316 -- Playlists 310 317 ----------------------------------------- 318 + ActivatePlaylist a -> 319 + Playlists.activate a 320 + 311 321 UI.AddTracksToPlaylist a -> 312 322 Playlists.addTracksToPlaylist a 313 323 324 + CreatePlaylist -> 325 + Playlists.create 326 + 327 + DeactivatePlaylist -> 328 + Playlists.deactivate 329 + 330 + DeletePlaylist a -> 331 + Playlists.delete a 332 + 333 + ModifyPlaylist -> 334 + Playlists.modify 335 + 336 + SetPlaylistCreationContext a -> 337 + Playlists.setCreationContext a 338 + 339 + SetPlaylistModificationContext a b -> 340 + Playlists.setModificationContext a b 341 + 342 + ShowPlaylistListMenu a b -> 343 + Playlists.showListMenu a b 344 + 314 345 ----------------------------------------- 315 346 -- Routing 316 347 ----------------------------------------- ··· 392 423 , update = Authentication.update 393 424 } 394 425 { model = model.authentication 395 - , msg = sub 396 - } 397 - 398 - PlaylistsMsg sub -> 399 - \model -> 400 - Return3.wieldNested 401 - Reply.translate 402 - { mapCmd = PlaylistsMsg 403 - , mapModel = \child -> { model | playlists = child } 404 - , update = Playlists.update 405 - } 406 - { model = model.playlists 407 426 , msg = sub 408 427 } 409 428
-546
src/Applications/UI/Playlists.elm
··· 1 - module UI.Playlists exposing (Model, Msg(..), importHypaethral, initialModel, update, view) 2 - 3 - import Chunky exposing (..) 4 - import Color exposing (Color) 5 - import Color.Ext as Color 6 - import Common 7 - import Conditional exposing (ifThenElse) 8 - import Coordinates 9 - import Css.Classes as C 10 - import Html exposing (Html, text) 11 - import Html.Attributes exposing (href, placeholder, style, value) 12 - import Html.Events exposing (onInput, onSubmit) 13 - import Html.Events.Extra.Mouse as Mouse 14 - import List.Extra as List 15 - import Material.Icons as Icons 16 - import Material.Icons.Types exposing (Coloring(..)) 17 - import Playlists exposing (..) 18 - import Return3 exposing (..) 19 - import UI.Kit exposing (ButtonType(..)) 20 - import UI.List 21 - import UI.Navigation exposing (..) 22 - import UI.Page as Page 23 - import UI.Playlists.Directory 24 - import UI.Playlists.Page exposing (Page(..)) 25 - import UI.Reply exposing (Reply(..)) 26 - import Url 27 - import User.Layer exposing (HypaethralData) 28 - 29 - 30 - 31 - -- 🌳 32 - 33 - 34 - type alias Model = 35 - { collection : List Playlist 36 - , lastModifiedPlaylist : Maybe String 37 - , newContext : Maybe String 38 - , editContext : Maybe { oldName : String, newName : String } 39 - , playlistToActivate : Maybe String 40 - } 41 - 42 - 43 - initialModel : Model 44 - initialModel = 45 - { collection = [] 46 - , lastModifiedPlaylist = Nothing 47 - , newContext = Nothing 48 - , editContext = Nothing 49 - , playlistToActivate = Nothing 50 - } 51 - 52 - 53 - 54 - -- 📣 55 - 56 - 57 - type Msg 58 - = Activate Playlist 59 - | Bypass 60 - | Create 61 - | Deactivate 62 - | Modify 63 - | RemoveFromCollection { playlistName : String } 64 - | SetCreationContext String 65 - | SetModificationContext String String 66 - | ShowListMenu Playlist Mouse.Event 67 - 68 - 69 - update : Msg -> Model -> Return Model Msg Reply 70 - update msg model = 71 - case msg of 72 - Activate playlist -> 73 - returnRepliesWithModel 74 - model 75 - [ ActivatePlaylist playlist 76 - , GoToPage Page.Index 77 - ] 78 - 79 - Create -> 80 - case model.newContext of 81 - Just playlistName -> 82 - let 83 - alreadyExists = 84 - List.any 85 - (.name >> (==) playlistName) 86 - (List.filterNot .autoGenerated model.collection) 87 - 88 - playlist = 89 - { autoGenerated = False 90 - , name = playlistName 91 - , tracks = [] 92 - } 93 - in 94 - if alreadyExists then 95 - returnReplyWithModel 96 - model 97 - (ShowErrorNotification "There's already a playlist with this name") 98 - 99 - else 100 - returnRepliesWithModel 101 - { model 102 - | collection = playlist :: model.collection 103 - , lastModifiedPlaylist = Just playlist.name 104 - , newContext = Nothing 105 - } 106 - [ GoToPage (Page.Playlists Index) 107 - , SavePlaylists 108 - ] 109 - 110 - Nothing -> 111 - return model 112 - 113 - Bypass -> 114 - return model 115 - 116 - Deactivate -> 117 - returnReplyWithModel 118 - model 119 - DeactivatePlaylist 120 - 121 - Modify -> 122 - case model.editContext of 123 - Just { oldName, newName } -> 124 - let 125 - properName = 126 - String.trim newName 127 - 128 - validName = 129 - String.isEmpty properName == False 130 - 131 - ( autoGenerated, notAutoGenerated ) = 132 - List.partition .autoGenerated model.collection 133 - 134 - alreadyExists = 135 - List.any 136 - (.name >> (==) properName) 137 - notAutoGenerated 138 - 139 - newCollection = 140 - List.map 141 - (\p -> ifThenElse (p.name == oldName) { p | name = properName } p) 142 - notAutoGenerated 143 - in 144 - if alreadyExists then 145 - returnReplyWithModel 146 - { model | editContext = Nothing } 147 - (ShowErrorNotification "There's already a playlist with this name") 148 - 149 - else if validName then 150 - returnRepliesWithModel 151 - { model 152 - | collection = newCollection ++ autoGenerated 153 - , lastModifiedPlaylist = Just properName 154 - , editContext = Nothing 155 - } 156 - [ GoToPage (Page.Playlists Index) 157 - , SavePlaylists 158 - ] 159 - 160 - else 161 - returnRepliesWithModel 162 - model 163 - [ GoToPage (Page.Playlists Index) ] 164 - 165 - Nothing -> 166 - returnRepliesWithModel 167 - model 168 - [ GoToPage (Page.Playlists Index) ] 169 - 170 - RemoveFromCollection { playlistName } -> 171 - model.collection 172 - |> List.filter 173 - (\p -> 174 - if p.autoGenerated then 175 - True 176 - 177 - else 178 - p.name /= playlistName 179 - ) 180 - |> (\col -> { model | collection = col }) 181 - |> return 182 - |> addReply SavePlaylists 183 - 184 - SetCreationContext playlistName -> 185 - return { model | newContext = Just playlistName } 186 - 187 - SetModificationContext oldName newName -> 188 - return { model | editContext = Just { oldName = oldName, newName = newName } } 189 - 190 - ShowListMenu playlist mouseEvent -> 191 - let 192 - coordinates = 193 - Coordinates.fromTuple mouseEvent.clientPos 194 - in 195 - returnRepliesWithModel model [ ShowPlaylistListMenu coordinates playlist ] 196 - 197 - 198 - importHypaethral : Model -> HypaethralData -> Return Model Msg Reply 199 - importHypaethral model data = 200 - return 201 - { model 202 - | collection = data.playlists ++ UI.Playlists.Directory.generate data.sources data.tracks 203 - , playlistToActivate = Nothing 204 - } 205 - 206 - 207 - 208 - -- 🗺 209 - 210 - 211 - view : Page -> Model -> Maybe Playlist -> Maybe Color -> Html Msg 212 - view page model selectedPlaylist bgColor = 213 - UI.Kit.receptacle 214 - { scrolling = True } 215 - (case page of 216 - Edit encodedName -> 217 - let 218 - playlists = 219 - List.filter 220 - (.autoGenerated >> (==) False) 221 - model.collection 222 - in 223 - encodedName 224 - |> Url.percentDecode 225 - |> Maybe.andThen (\n -> List.find (.name >> (==) n) playlists) 226 - |> Maybe.map (edit model) 227 - |> Maybe.withDefault [ nothing ] 228 - 229 - Index -> 230 - index model selectedPlaylist bgColor 231 - 232 - New -> 233 - new model 234 - ) 235 - 236 - 237 - 238 - -- INDEX 239 - 240 - 241 - index : Model -> Maybe Playlist -> Maybe Color -> List (Html Msg) 242 - index model selectedPlaylist bgColor = 243 - let 244 - selectedPlaylistName = 245 - Maybe.map .name selectedPlaylist 246 - 247 - customPlaylists = 248 - model.collection 249 - |> List.filterNot .autoGenerated 250 - |> List.sortBy .name 251 - 252 - customPlaylistListItem playlist = 253 - if selectedPlaylistName == Just playlist.name then 254 - selectedPlaylistListItem playlist bgColor 255 - 256 - else 257 - { label = text playlist.name 258 - , actions = 259 - [ { icon = Icons.more_vert 260 - , msg = Just (ShowListMenu playlist) 261 - , title = "Menu" 262 - } 263 - ] 264 - , msg = Just (Activate playlist) 265 - , isSelected = False 266 - } 267 - 268 - directoryPlaylists = 269 - model.collection 270 - |> List.filter .autoGenerated 271 - |> List.sortBy .name 272 - 273 - directoryPlaylistListItem playlist = 274 - if selectedPlaylistName == Just playlist.name then 275 - selectedPlaylistListItem playlist bgColor 276 - 277 - else 278 - { label = text playlist.name 279 - , actions = [] 280 - , msg = Just (Activate playlist) 281 - , isSelected = False 282 - } 283 - in 284 - [ ----------------------------------------- 285 - -- Navigation 286 - ----------------------------------------- 287 - UI.Navigation.local 288 - [ ( Icon Icons.arrow_back 289 - , Label Common.backToIndex Hidden 290 - , NavigateToPage Page.Index 291 - ) 292 - , ( Icon Icons.add 293 - , Label "Create a new playlist" Shown 294 - , NavigateToPage (Page.Playlists New) 295 - ) 296 - ] 297 - 298 - ----------------------------------------- 299 - -- Content 300 - ----------------------------------------- 301 - , if List.isEmpty model.collection then 302 - chunk 303 - [ C.relative ] 304 - [ chunk 305 - [ C.absolute, C.left_0, C.top_0 ] 306 - [ UI.Kit.canister [ UI.Kit.h1 "Playlists" ] ] 307 - ] 308 - 309 - else 310 - UI.Kit.canister 311 - [ UI.Kit.h1 "Playlists" 312 - 313 - -- Intro 314 - -------- 315 - , intro 316 - 317 - -- Custom Playlists 318 - ------------------- 319 - , if List.isEmpty customPlaylists then 320 - nothing 321 - 322 - else 323 - raw 324 - [ category "Your Playlists" 325 - , UI.List.view 326 - UI.List.Normal 327 - (List.map customPlaylistListItem customPlaylists) 328 - ] 329 - 330 - -- Directory Playlists 331 - ---------------------- 332 - , if List.isEmpty directoryPlaylists then 333 - nothing 334 - 335 - else 336 - raw 337 - [ category "Autogenerated Directory Playlists" 338 - , UI.List.view 339 - UI.List.Normal 340 - (List.map directoryPlaylistListItem directoryPlaylists) 341 - ] 342 - ] 343 - 344 - -- 345 - , if List.isEmpty model.collection then 346 - UI.Kit.centeredContent 347 - [ slab 348 - Html.a 349 - [ href (Page.toString <| Page.Playlists New) ] 350 - [ C.block 351 - , C.opacity_30 352 - , C.text_inherit 353 - ] 354 - [ Icons.waves 64 Inherit ] 355 - , slab 356 - Html.a 357 - [ href (Page.toString <| Page.Playlists New) ] 358 - [ C.block 359 - , C.leading_normal 360 - , C.mt_2 361 - , C.opacity_40 362 - , C.text_center 363 - , C.text_inherit 364 - ] 365 - [ text "No playlists found, create one" 366 - , lineBreak 367 - , text "or enable directory playlists." 368 - ] 369 - ] 370 - 371 - else 372 - nothing 373 - ] 374 - 375 - 376 - intro : Html Msg 377 - intro = 378 - [ text "Playlists are not tied to the sources of its tracks, " 379 - , text "same goes for favourites." 380 - , lineBreak 381 - , text "There's also directory playlists, which are playlists derived from root directories." 382 - ] 383 - |> raw 384 - |> UI.Kit.intro 385 - 386 - 387 - category : String -> Html Msg 388 - category cat = 389 - chunk 390 - [ C.antialiased 391 - , C.font_display 392 - , C.mb_3 393 - , C.mt_10 394 - , C.text_base05 395 - , C.text_xxs 396 - , C.truncate 397 - , C.uppercase 398 - 399 - -- Dark mode 400 - ------------ 401 - , C.dark__text_base04 402 - ] 403 - [ UI.Kit.inlineIcon Icons.folder 404 - , inline [ C.font_bold, C.ml_2 ] [ text cat ] 405 - ] 406 - 407 - 408 - selectedPlaylistListItem : Playlist -> Maybe Color -> UI.List.Item Msg 409 - selectedPlaylistListItem playlist bgColor = 410 - let 411 - selectionColor = 412 - Maybe.withDefault UI.Kit.colors.selection bgColor 413 - in 414 - { label = 415 - brick 416 - [ selectionColor 417 - |> Color.toCssString 418 - |> style "color" 419 - ] 420 - [] 421 - [ text playlist.name ] 422 - , actions = 423 - [ { icon = \size _ -> Icons.check size (Color selectionColor) 424 - , msg = Nothing 425 - , title = "Selected playlist" 426 - } 427 - ] 428 - , msg = Just Deactivate 429 - , isSelected = False 430 - } 431 - 432 - 433 - 434 - -- NEW 435 - 436 - 437 - new : Model -> List (Html Msg) 438 - new _ = 439 - [ ----------------------------------------- 440 - -- Navigation 441 - ----------------------------------------- 442 - UI.Navigation.local 443 - [ ( Icon Icons.arrow_back 444 - , Label "Back to list" Hidden 445 - , NavigateToPage (Page.Playlists Index) 446 - ) 447 - ] 448 - 449 - ----------------------------------------- 450 - -- Content 451 - ----------------------------------------- 452 - , [ UI.Kit.h2 "Name your playlist" 453 - 454 - -- 455 - , [ onInput SetCreationContext 456 - , placeholder "The Classics" 457 - ] 458 - |> UI.Kit.textField 459 - |> chunky [ C.max_w_md, C.mx_auto ] 460 - 461 - -- Button 462 - --------- 463 - , chunk 464 - [ C.mt_10 ] 465 - [ UI.Kit.button 466 - Normal 467 - Bypass 468 - (text "Create playlist") 469 - ] 470 - ] 471 - |> UI.Kit.canisterForm 472 - |> List.singleton 473 - |> UI.Kit.centeredContent 474 - |> List.singleton 475 - |> slab 476 - Html.form 477 - [ onSubmit Create ] 478 - [ C.flex 479 - , C.flex_grow 480 - , C.text_center 481 - ] 482 - ] 483 - 484 - 485 - 486 - -- EDIT 487 - 488 - 489 - edit : Model -> Playlist -> List (Html Msg) 490 - edit model playlist = 491 - [ ----------------------------------------- 492 - -- Navigation 493 - ----------------------------------------- 494 - UI.Navigation.local 495 - [ ( Icon Icons.arrow_back 496 - , Label "Back to list" Hidden 497 - , NavigateToPage (Page.Playlists Index) 498 - ) 499 - ] 500 - 501 - ----------------------------------------- 502 - -- Content 503 - ----------------------------------------- 504 - , [ UI.Kit.h2 "Name your playlist" 505 - 506 - -- 507 - , [ onInput (SetModificationContext playlist.name) 508 - , placeholder "The Classics" 509 - 510 - -- 511 - , case model.editContext of 512 - Just { oldName, newName } -> 513 - if playlist.name == oldName then 514 - value newName 515 - 516 - else 517 - value playlist.name 518 - 519 - Nothing -> 520 - value playlist.name 521 - ] 522 - |> UI.Kit.textField 523 - |> chunky [ C.max_w_md, C.mx_auto ] 524 - 525 - -- Button 526 - --------- 527 - , chunk 528 - [ C.mt_10 ] 529 - [ UI.Kit.button 530 - Normal 531 - Bypass 532 - (text "Save") 533 - ] 534 - ] 535 - |> UI.Kit.canisterForm 536 - |> List.singleton 537 - |> UI.Kit.centeredContent 538 - |> List.singleton 539 - |> slab 540 - Html.form 541 - [ onSubmit Modify ] 542 - [ C.flex 543 - , C.flex_grow 544 - , C.text_center 545 - ] 546 - ]
+182 -14
src/Applications/UI/Playlists/State.elm
··· 1 1 module UI.Playlists.State exposing (..) 2 2 3 + import Alien 4 + import Conditional exposing (ifThenElse) 5 + import Coordinates 6 + import Html.Events.Extra.Mouse as Mouse 7 + import Json.Encode 3 8 import List.Extra as List 4 9 import Monocle.Lens as Lens 5 10 import Notifications 6 - import Playlists exposing (PlaylistTrack) 11 + import Playlists exposing (..) 12 + import Playlists.Encoding as Playlists 7 13 import Return exposing (andThen, return) 8 14 import Return.Ext as Return exposing (communicate) 9 15 import UI.Common.State as Common 10 - import UI.Reply exposing (Reply(..)) 11 - import UI.Reply.Translate as Reply 12 - import UI.Types as UI exposing (Manager, Msg(..)) 16 + import UI.Page as Page 17 + import UI.Playlists.ContextMenu as Playlists 18 + import UI.Playlists.Page exposing (..) 19 + import UI.Ports as Ports 20 + import UI.Routing.State as Routing 21 + import UI.Tracks as Tracks 22 + import UI.Types as UI exposing (..) 13 23 14 24 15 25 16 26 -- 🔱 17 27 18 28 29 + activate : Playlist -> Manager 30 + activate playlist model = 31 + playlist 32 + |> Tracks.SelectPlaylist 33 + |> TracksMsg 34 + |> Return.performanceF model 35 + |> andThen redirectToIndexPage 36 + 37 + 19 38 addTracksToPlaylist : { playlistName : String, tracks : List PlaylistTrack } -> Manager 20 39 addTracksToPlaylist { playlistName, tracks } model = 21 40 let ··· 25 44 playlistIndex = 26 45 List.findIndex 27 46 (\p -> p.autoGenerated == False && p.name == properPlaylistName) 28 - model.playlists.collection 29 - 30 - playlistsModel = 31 - model.playlists 47 + model.playlists 32 48 33 49 newCollection = 34 50 case playlistIndex of ··· 36 52 List.updateAt 37 53 idx 38 54 (\p -> { p | tracks = p.tracks ++ tracks }) 39 - playlistsModel.collection 55 + model.playlists 40 56 41 57 Nothing -> 42 58 (::) ··· 44 60 , name = properPlaylistName 45 61 , tracks = tracks 46 62 } 47 - playlistsModel.collection 63 + model.playlists 48 64 49 65 newModel = 50 - { playlistsModel 51 - | collection = newCollection 66 + { model 67 + | playlists = newCollection 52 68 , lastModifiedPlaylist = Just properPlaylistName 53 69 } 54 - |> (\m -> { model | playlists = m }) 55 70 in 56 71 (case tracks of 57 72 [ t ] -> ··· 63 78 |> (\s -> s ++ " to the __" ++ properPlaylistName ++ "__ playlist") 64 79 |> Notifications.success 65 80 |> Common.showNotificationWithModel newModel 66 - |> andThen (Reply.translate SavePlaylists) 81 + |> andThen save 82 + 83 + 84 + create : Manager 85 + create model = 86 + case model.newPlaylistContext of 87 + Just playlistName -> 88 + let 89 + alreadyExists = 90 + List.any 91 + (.name >> (==) playlistName) 92 + (List.filterNot .autoGenerated model.playlists) 93 + 94 + playlist = 95 + { autoGenerated = False 96 + , name = playlistName 97 + , tracks = [] 98 + } 99 + in 100 + if alreadyExists then 101 + "There's already a playlist with this name" 102 + |> Notifications.error 103 + |> Common.showNotificationWithModel model 104 + 105 + else 106 + { model 107 + | lastModifiedPlaylist = Just playlist.name 108 + , newPlaylistContext = Nothing 109 + , playlists = playlist :: model.playlists 110 + } 111 + |> save 112 + |> andThen redirectToIndexPage 113 + 114 + Nothing -> 115 + Return.singleton model 116 + 117 + 118 + deactivate : Manager 119 + deactivate model = 120 + Tracks.DeselectPlaylist 121 + |> TracksMsg 122 + |> Return.performanceF model 123 + 124 + 125 + delete : { playlistName : String } -> Manager 126 + delete { playlistName } model = 127 + model.playlists 128 + |> List.filter 129 + (\p -> 130 + if p.autoGenerated then 131 + True 132 + 133 + else 134 + p.name /= playlistName 135 + ) 136 + |> (\col -> { model | playlists = col }) 137 + |> save 138 + 139 + 140 + modify : Manager 141 + modify model = 142 + case model.editPlaylistContext of 143 + Just { oldName, newName } -> 144 + let 145 + properName = 146 + String.trim newName 147 + 148 + validName = 149 + String.isEmpty properName == False 150 + 151 + ( autoGenerated, notAutoGenerated ) = 152 + List.partition .autoGenerated model.playlists 153 + 154 + alreadyExists = 155 + List.any 156 + (.name >> (==) properName) 157 + notAutoGenerated 158 + 159 + newCollection = 160 + List.map 161 + (\p -> ifThenElse (p.name == oldName) { p | name = properName } p) 162 + notAutoGenerated 163 + in 164 + if alreadyExists then 165 + "There's already a playlist with this name" 166 + |> Notifications.error 167 + |> Common.showNotificationWithModel 168 + { model | editPlaylistContext = Nothing } 169 + 170 + else if validName then 171 + { model 172 + | editPlaylistContext = Nothing 173 + , lastModifiedPlaylist = Just properName 174 + , playlists = newCollection ++ autoGenerated 175 + } 176 + |> save 177 + |> andThen redirectToIndexPage 178 + 179 + else 180 + redirectToIndexPage model 181 + 182 + Nothing -> 183 + redirectToIndexPage model 184 + 185 + 186 + save : Manager 187 + save model = 188 + model.playlists 189 + |> List.filterNot .autoGenerated 190 + |> Json.Encode.list Playlists.encode 191 + |> Alien.broadcast Alien.SavePlaylists 192 + |> Ports.toBrain 193 + |> return model 194 + 195 + 196 + setCreationContext : String -> Manager 197 + setCreationContext playlistName model = 198 + Return.singleton { model | newPlaylistContext = Just playlistName } 199 + 200 + 201 + setModificationContext : String -> String -> Manager 202 + setModificationContext oldName newName model = 203 + let 204 + context = 205 + { oldName = oldName 206 + , newName = newName 207 + } 208 + in 209 + Return.singleton { model | editPlaylistContext = Just context } 210 + 211 + 212 + showListMenu : Playlist -> Mouse.Event -> Manager 213 + showListMenu playlist mouseEvent model = 214 + let 215 + coordinates = 216 + Coordinates.fromTuple mouseEvent.clientPos 217 + 218 + contextMenu = 219 + Playlists.listMenu 220 + playlist 221 + model.tracks.collection.identified 222 + model.confirmation 223 + coordinates 224 + in 225 + Return.singleton { model | contextMenu = Just contextMenu } 226 + 227 + 228 + 229 + -- ㊙️ 230 + 231 + 232 + redirectToIndexPage : Manager 233 + redirectToIndexPage = 234 + Routing.changeUrlUsingPage (Page.Playlists Index)
+366
src/Applications/UI/Playlists/View.elm
··· 1 + module UI.Playlists.View exposing (view) 2 + 3 + import Chunky exposing (..) 4 + import Color exposing (Color) 5 + import Color.Ext as Color 6 + import Common 7 + import Conditional exposing (ifThenElse) 8 + import Coordinates 9 + import Css.Classes as C 10 + import Html exposing (Html, text) 11 + import Html.Attributes exposing (href, placeholder, style, value) 12 + import Html.Events exposing (onInput, onSubmit) 13 + import Html.Events.Extra.Mouse as Mouse 14 + import List.Extra as List 15 + import Material.Icons as Icons 16 + import Material.Icons.Types exposing (Coloring(..)) 17 + import Playlists exposing (..) 18 + import UI.Kit exposing (ButtonType(..)) 19 + import UI.List 20 + import UI.Navigation exposing (..) 21 + import UI.Page as Page 22 + import UI.Playlists.Page exposing (..) 23 + import UI.Types exposing (..) 24 + import Url 25 + 26 + 27 + 28 + -- 🗺 29 + 30 + 31 + view : Page -> List Playlist -> Maybe Playlist -> Maybe { oldName : String, newName : String } -> Maybe Color -> Html Msg 32 + view page playlists selectedPlaylist editContext bgColor = 33 + UI.Kit.receptacle 34 + { scrolling = True } 35 + (case page of 36 + Edit encodedName -> 37 + let 38 + filtered = 39 + List.filter 40 + (.autoGenerated >> (==) False) 41 + playlists 42 + in 43 + encodedName 44 + |> Url.percentDecode 45 + |> Maybe.andThen (\n -> List.find (.name >> (==) n) filtered) 46 + |> Maybe.map (edit editContext) 47 + |> Maybe.withDefault [ nothing ] 48 + 49 + Index -> 50 + index playlists selectedPlaylist bgColor 51 + 52 + New -> 53 + new 54 + ) 55 + 56 + 57 + 58 + -- INDEX 59 + 60 + 61 + index : List Playlist -> Maybe Playlist -> Maybe Color -> List (Html Msg) 62 + index playlists selectedPlaylist bgColor = 63 + let 64 + selectedPlaylistName = 65 + Maybe.map .name selectedPlaylist 66 + 67 + customPlaylists = 68 + playlists 69 + |> List.filterNot .autoGenerated 70 + |> List.sortBy .name 71 + 72 + customPlaylistListItem playlist = 73 + if selectedPlaylistName == Just playlist.name then 74 + selectedPlaylistListItem playlist bgColor 75 + 76 + else 77 + { label = text playlist.name 78 + , actions = 79 + [ { icon = Icons.more_vert 80 + , msg = Just (ShowPlaylistListMenu playlist) 81 + , title = "Menu" 82 + } 83 + ] 84 + , msg = Just (ActivatePlaylist playlist) 85 + , isSelected = False 86 + } 87 + 88 + directoryPlaylists = 89 + playlists 90 + |> List.filter .autoGenerated 91 + |> List.sortBy .name 92 + 93 + directoryPlaylistListItem playlist = 94 + if selectedPlaylistName == Just playlist.name then 95 + selectedPlaylistListItem playlist bgColor 96 + 97 + else 98 + { label = text playlist.name 99 + , actions = [] 100 + , msg = Just (ActivatePlaylist playlist) 101 + , isSelected = False 102 + } 103 + in 104 + [ ----------------------------------------- 105 + -- Navigation 106 + ----------------------------------------- 107 + UI.Navigation.local 108 + [ ( Icon Icons.arrow_back 109 + , Label Common.backToIndex Hidden 110 + , NavigateToPage Page.Index 111 + ) 112 + , ( Icon Icons.add 113 + , Label "Create a new playlist" Shown 114 + , NavigateToPage (Page.Playlists New) 115 + ) 116 + ] 117 + 118 + ----------------------------------------- 119 + -- Content 120 + ----------------------------------------- 121 + , if List.isEmpty playlists then 122 + chunk 123 + [ C.relative ] 124 + [ chunk 125 + [ C.absolute, C.left_0, C.top_0 ] 126 + [ UI.Kit.canister [ UI.Kit.h1 "Playlists" ] ] 127 + ] 128 + 129 + else 130 + UI.Kit.canister 131 + [ UI.Kit.h1 "Playlists" 132 + 133 + -- Intro 134 + -------- 135 + , intro 136 + 137 + -- Custom Playlists 138 + ------------------- 139 + , if List.isEmpty customPlaylists then 140 + nothing 141 + 142 + else 143 + raw 144 + [ category "Your Playlists" 145 + , UI.List.view 146 + UI.List.Normal 147 + (List.map customPlaylistListItem customPlaylists) 148 + ] 149 + 150 + -- Directory Playlists 151 + ---------------------- 152 + , if List.isEmpty directoryPlaylists then 153 + nothing 154 + 155 + else 156 + raw 157 + [ category "Autogenerated Directory Playlists" 158 + , UI.List.view 159 + UI.List.Normal 160 + (List.map directoryPlaylistListItem directoryPlaylists) 161 + ] 162 + ] 163 + 164 + -- 165 + , if List.isEmpty playlists then 166 + UI.Kit.centeredContent 167 + [ slab 168 + Html.a 169 + [ href (Page.toString <| Page.Playlists New) ] 170 + [ C.block 171 + , C.opacity_30 172 + , C.text_inherit 173 + ] 174 + [ Icons.waves 64 Inherit ] 175 + , slab 176 + Html.a 177 + [ href (Page.toString <| Page.Playlists New) ] 178 + [ C.block 179 + , C.leading_normal 180 + , C.mt_2 181 + , C.opacity_40 182 + , C.text_center 183 + , C.text_inherit 184 + ] 185 + [ text "No playlists found, create one" 186 + , lineBreak 187 + , text "or enable directory playlists." 188 + ] 189 + ] 190 + 191 + else 192 + nothing 193 + ] 194 + 195 + 196 + intro : Html Msg 197 + intro = 198 + [ text "Playlists are not tied to the sources of its tracks, " 199 + , text "same goes for favourites." 200 + , lineBreak 201 + , text "There's also directory playlists, which are playlists derived from root directories." 202 + ] 203 + |> raw 204 + |> UI.Kit.intro 205 + 206 + 207 + category : String -> Html Msg 208 + category cat = 209 + chunk 210 + [ C.antialiased 211 + , C.font_display 212 + , C.mb_3 213 + , C.mt_10 214 + , C.text_base05 215 + , C.text_xxs 216 + , C.truncate 217 + , C.uppercase 218 + 219 + -- Dark mode 220 + ------------ 221 + , C.dark__text_base04 222 + ] 223 + [ UI.Kit.inlineIcon Icons.folder 224 + , inline [ C.font_bold, C.ml_2 ] [ text cat ] 225 + ] 226 + 227 + 228 + selectedPlaylistListItem : Playlist -> Maybe Color -> UI.List.Item Msg 229 + selectedPlaylistListItem playlist bgColor = 230 + let 231 + selectionColor = 232 + Maybe.withDefault UI.Kit.colors.selection bgColor 233 + in 234 + { label = 235 + brick 236 + [ selectionColor 237 + |> Color.toCssString 238 + |> style "color" 239 + ] 240 + [] 241 + [ text playlist.name ] 242 + , actions = 243 + [ { icon = \size _ -> Icons.check size (Color selectionColor) 244 + , msg = Nothing 245 + , title = "Selected playlist" 246 + } 247 + ] 248 + , msg = Just DeactivatePlaylist 249 + , isSelected = False 250 + } 251 + 252 + 253 + 254 + -- NEW 255 + 256 + 257 + new : List (Html Msg) 258 + new = 259 + [ ----------------------------------------- 260 + -- Navigation 261 + ----------------------------------------- 262 + UI.Navigation.local 263 + [ ( Icon Icons.arrow_back 264 + , Label "Back to list" Hidden 265 + , NavigateToPage (Page.Playlists Index) 266 + ) 267 + ] 268 + 269 + ----------------------------------------- 270 + -- Content 271 + ----------------------------------------- 272 + , [ UI.Kit.h2 "Name your playlist" 273 + 274 + -- 275 + , [ onInput SetPlaylistCreationContext 276 + , placeholder "The Classics" 277 + ] 278 + |> UI.Kit.textField 279 + |> chunky [ C.max_w_md, C.mx_auto ] 280 + 281 + -- Button 282 + --------- 283 + , chunk 284 + [ C.mt_10 ] 285 + [ UI.Kit.button 286 + Normal 287 + Bypass 288 + (text "Create playlist") 289 + ] 290 + ] 291 + |> UI.Kit.canisterForm 292 + |> List.singleton 293 + |> UI.Kit.centeredContent 294 + |> List.singleton 295 + |> slab 296 + Html.form 297 + [ onSubmit CreatePlaylist ] 298 + [ C.flex 299 + , C.flex_grow 300 + , C.text_center 301 + ] 302 + ] 303 + 304 + 305 + 306 + -- EDIT 307 + 308 + 309 + edit : Maybe { oldName : String, newName : String } -> Playlist -> List (Html Msg) 310 + edit editContext playlist = 311 + [ ----------------------------------------- 312 + -- Navigation 313 + ----------------------------------------- 314 + UI.Navigation.local 315 + [ ( Icon Icons.arrow_back 316 + , Label "Back to list" Hidden 317 + , NavigateToPage (Page.Playlists Index) 318 + ) 319 + ] 320 + 321 + ----------------------------------------- 322 + -- Content 323 + ----------------------------------------- 324 + , [ UI.Kit.h2 "Name your playlist" 325 + 326 + -- 327 + , [ onInput (SetPlaylistModificationContext playlist.name) 328 + , placeholder "The Classics" 329 + 330 + -- 331 + , case editContext of 332 + Just { oldName, newName } -> 333 + if playlist.name == oldName then 334 + value newName 335 + 336 + else 337 + value playlist.name 338 + 339 + Nothing -> 340 + value playlist.name 341 + ] 342 + |> UI.Kit.textField 343 + |> chunky [ C.max_w_md, C.mx_auto ] 344 + 345 + -- Button 346 + --------- 347 + , chunk 348 + [ C.mt_10 ] 349 + [ UI.Kit.button 350 + Normal 351 + Bypass 352 + (text "Save") 353 + ] 354 + ] 355 + |> UI.Kit.canisterForm 356 + |> List.singleton 357 + |> UI.Kit.centeredContent 358 + |> List.singleton 359 + |> slab 360 + Html.form 361 + [ onSubmit ModifyPlaylist ] 362 + [ C.flex 363 + , C.flex_grow 364 + , C.text_center 365 + ] 366 + ]
+29 -47
src/Applications/UI/Reply/Translate.elm
··· 47 47 import UI.Navigation as Navigation 48 48 import UI.Notifications 49 49 import UI.Page as Page 50 - import UI.Playlists as Playlists 51 50 import UI.Playlists.Alfred 52 51 import UI.Playlists.ContextMenu as Playlists 53 52 import UI.Playlists.Directory ··· 186 185 187 186 SignOut -> 188 187 let 189 - { playlists, sources, tracks } = 188 + { sources, tracks } = 190 189 model 191 190 in 192 191 { model 193 192 | authentication = Authentication.Unauthenticated 194 - , playlists = 195 - { playlists 196 - | collection = [] 197 - , playlistToActivate = Maybe.map .name tracks.selectedPlaylist 198 - } 193 + , playlists = [] 194 + , playlistToActivate = Nothing 195 + 196 + -- 199 197 , queue = 200 198 Queue.initialModel 201 199 , sources = ··· 238 236 ShowMoreAuthenticationOptions coordinates -> 239 237 Return.singleton { model | contextMenu = Just (Authentication.moreOptionsMenu coordinates) } 240 238 241 - ShowPlaylistListMenu coordinates playlist -> 239 + Reply.ShowPlaylistListMenu coordinates playlist -> 242 240 Return.singleton { model | contextMenu = Just (Playlists.listMenu playlist model.tracks.collection.identified model.confirmation coordinates) } 243 241 244 242 ShowQueueFutureMenu coordinates { item, itemIndex } -> ··· 257 255 , cachingInProgress = model.tracks.cachingInProgress 258 256 , currentTime = model.currentTime 259 257 , selectedPlaylist = model.tracks.selectedPlaylist 260 - , lastModifiedPlaylistName = model.playlists.lastModifiedPlaylist 258 + , lastModifiedPlaylistName = model.lastModifiedPlaylist 261 259 , showAlternativeMenu = alt 262 260 , sources = model.sources.collection 263 261 } ··· 328 326 ----------------------------------------- 329 327 -- Playlists 330 328 ----------------------------------------- 331 - ActivatePlaylist playlist -> 329 + Reply.ActivatePlaylist playlist -> 332 330 playlist 333 331 |> Tracks.SelectPlaylist 334 332 |> TracksMsg ··· 337 335 Reply.AddTracksToPlaylist a -> 338 336 Return.performance (UI.AddTracksToPlaylist a) model 339 337 340 - DeactivatePlaylist -> 338 + Reply.DeactivatePlaylist -> 341 339 Tracks.DeselectPlaylist 342 340 |> TracksMsg 343 341 |> Return.performanceF model ··· 347 345 nonDirectoryPlaylists = 348 346 List.filterNot 349 347 .autoGenerated 350 - model.playlists.collection 348 + model.playlists 351 349 352 350 directoryPlaylists = 353 351 UI.Playlists.Directory.generate 354 352 model.sources.collection 355 353 model.tracks.collection.untouched 356 - 357 - playlists = 358 - model.playlists 359 354 in 360 355 [ nonDirectoryPlaylists 361 356 , directoryPlaylists 362 357 ] 363 358 |> List.concat 364 - |> (\c -> { playlists | collection = c }) 365 - |> (\p -> { model | playlists = p }) 359 + |> (\c -> { model | playlists = c }) 366 360 |> Return.singleton 367 361 368 362 RemoveFromSelectedPlaylist playlist tracks -> ··· 370 364 updatedPlaylist = 371 365 Tracks.removeFromPlaylist tracks playlist 372 366 373 - ( tracksModel, playlistsModel ) = 374 - ( model.tracks 375 - , model.playlists 376 - ) 377 - 378 - newPlaylistsCollection = 379 - List.map 380 - (\p -> 381 - if p.name == playlist.name then 382 - updatedPlaylist 367 + tracksModel = 368 + model.tracks 369 + in 370 + model.playlists 371 + |> List.map 372 + (\p -> 373 + if p.name == playlist.name then 374 + updatedPlaylist 383 375 384 - else 385 - p 386 - ) 387 - model.playlists.collection 388 - in 389 - newPlaylistsCollection 390 - |> (\c -> { playlistsModel | collection = c }) 391 - |> (\p -> { model | playlists = p }) 376 + else 377 + p 378 + ) 379 + |> (\c -> { model | playlists = c }) 392 380 |> Return.performance (TracksMsg <| Tracks.SelectPlaylist updatedPlaylist) 393 381 |> andThen (translate SavePlaylists) 394 382 395 383 RemovePlaylistFromCollection args -> 396 384 args 397 - |> Playlists.RemoveFromCollection 398 - |> PlaylistsMsg 385 + |> DeletePlaylist 399 386 |> Return.performanceF { model | confirmation = Nothing } 400 387 401 388 ReplacePlaylistInCollection playlist -> 402 - let 403 - playlists = 404 - model.playlists 405 - in 406 - playlists.collection 389 + model.playlists 407 390 |> List.map (\p -> ifThenElse (p.name == playlist.name) playlist p) 408 - |> (\c -> { playlists | collection = c }) 409 - |> (\p -> { model | playlists = p }) 391 + |> (\c -> { model | playlists = c }) 410 392 |> translate SavePlaylists 411 393 412 394 RequestAssistanceForPlaylists tracks -> 413 - model.playlists.collection 395 + model.playlists 414 396 |> List.filterNot .autoGenerated 415 397 |> UI.Playlists.Alfred.create tracks 416 398 |> AssignAlfred ··· 745 727 746 728 Export -> 747 729 { favourites = model.tracks.favourites 748 - , playlists = List.filterNot .autoGenerated model.playlists.collection 730 + , playlists = List.filterNot .autoGenerated model.playlists 749 731 , progress = model.progress 750 732 , settings = Just (gatherSettings model) 751 733 , sources = model.sources.collection ··· 782 764 |> return model 783 765 784 766 SavePlaylists -> 785 - model.playlists.collection 767 + model.playlists 786 768 |> List.filterNot .autoGenerated 787 769 |> Json.Encode.list Playlists.encode 788 770 |> Alien.broadcast Alien.SavePlaylists
+19 -4
src/Applications/UI/Types.elm
··· 19 19 import Html exposing (Html, section) 20 20 import Html.Attributes exposing (class, id, style) 21 21 import Html.Events exposing (on, onClick) 22 + import Html.Events.Extra.Mouse as Mouse 22 23 import Html.Events.Extra.Pointer as Pointer 23 24 import Html.Lazy as Lazy 24 25 import Http ··· 30 31 import Management 31 32 import Maybe.Extra as Maybe 32 33 import Notifications exposing (Notification) 33 - import Playlists exposing (PlaylistTrack) 34 + import Playlists exposing (Playlist, PlaylistTrack) 34 35 import Playlists.Encoding as Playlists 35 36 import Queue 36 37 import Sources ··· 46 47 import UI.Navigation as Navigation 47 48 import UI.Notifications 48 49 import UI.Page as Page exposing (Page) 49 - import UI.Playlists as Playlists 50 50 import UI.Playlists.ContextMenu as Playlists 51 51 import UI.Ports as Ports 52 52 import UI.Queue as Queue ··· 136 136 , notifications : UI.Notifications.Model 137 137 138 138 ----------------------------------------- 139 + -- Playlists 140 + ----------------------------------------- 141 + , editPlaylistContext : Maybe { oldName : String, newName : String } 142 + , lastModifiedPlaylist : Maybe String 143 + , newPlaylistContext : Maybe String 144 + , playlists : List Playlist 145 + , playlistToActivate : Maybe String 146 + 147 + ----------------------------------------- 139 148 -- Children (TODO) 140 149 ----------------------------------------- 141 150 , authentication : Authentication.Model 142 151 , queue : Queue.Model 143 - , playlists : Playlists.Model 144 152 , sources : Sources.Model 145 153 , tracks : Tracks.Model 146 154 } ··· 208 216 ----------------------------------------- 209 217 -- Playlists 210 218 ----------------------------------------- 219 + | ActivatePlaylist Playlist 211 220 | AddTracksToPlaylist { playlistName : String, tracks : List PlaylistTrack } 221 + | CreatePlaylist 222 + | DeactivatePlaylist 223 + | DeletePlaylist { playlistName : String } 224 + | ModifyPlaylist 225 + | SetPlaylistCreationContext String 226 + | SetPlaylistModificationContext String String 227 + | ShowPlaylistListMenu Playlist Mouse.Event 212 228 ----------------------------------------- 213 229 -- Routing 214 230 ----------------------------------------- ··· 248 264 -- Children (TODO) 249 265 ----------------------------------------- 250 266 | AuthenticationMsg Authentication.Msg 251 - | PlaylistsMsg Playlists.Msg 252 267 | QueueMsg Queue.Msg 253 268 | SourcesMsg Sources.Msg 254 269 | TracksMsg Tracks.Msg
+19 -21
src/Applications/UI/User/State/Import.elm
··· 18 18 import UI.Common.State as Common exposing (showNotification) 19 19 import UI.Equalizer.State as Equalizer 20 20 import UI.Page as Page exposing (Page) 21 - import UI.Playlists as Playlists 21 + import UI.Playlists.Directory 22 + import UI.Playlists.State as Playlists 22 23 import UI.Ports as Ports 23 24 import UI.Reply exposing (..) 24 25 import UI.Reply.Translate as Reply ··· 140 141 sourcesModel = 141 142 { sources | collection = data.sources } 142 143 143 - ( playlistsModel, playlistsCmd, playlistsReplies ) = 144 - Playlists.importHypaethral model.playlists data 144 + newPlaylistsCollection = 145 + List.append 146 + data.playlists 147 + (UI.Playlists.Directory.generate data.sources data.tracks) 145 148 146 149 selectedPlaylist = 147 150 Maybe.andThen 148 - (\n -> List.find (.name >> (==) n) playlistsModel.collection) 149 - model.playlists.playlistToActivate 151 + (\n -> List.find (.name >> (==) n) newPlaylistsCollection) 152 + model.playlistToActivate 150 153 151 154 ( tracksModel, tracksCmd, tracksReplies ) = 152 155 Tracks.importHypaethral model.tracks data selectedPlaylist ··· 155 158 model.lastFm 156 159 in 157 160 ( { model 158 - | playlists = playlistsModel 159 - , sources = sourcesModel 161 + | sources = sourcesModel 160 162 , tracks = tracksModel 161 163 162 164 -- 163 165 , chosenBackdrop = chosenBackdrop 164 166 , lastFm = { lastFmModel | sessionKey = Maybe.andThen .lastFm data.settings } 167 + , playlists = newPlaylistsCollection 168 + , playlistToActivate = Nothing 165 169 , processAutomatically = Maybe.unwrap True .processAutomatically data.settings 166 170 , progress = data.progress 167 171 , rememberProgress = Maybe.unwrap True .rememberProgress data.settings 168 172 } 169 173 -- 170 - , Cmd.batch 171 - [ Cmd.map PlaylistsMsg playlistsCmd 172 - , Cmd.map TracksMsg tracksCmd 173 - ] 174 + , Cmd.map TracksMsg tracksCmd 174 175 -- 175 - , playlistsReplies ++ tracksReplies 176 + , tracksReplies 176 177 ) 177 178 178 179 Err err -> ··· 189 190 importEnclosed : Json.Decode.Value -> Model -> Return3.Return Model Msg Reply 190 191 importEnclosed value model = 191 192 let 192 - { playlists, queue, tracks } = 193 + { queue, tracks } = 193 194 model 194 195 195 196 equalizerSettings = ··· 206 207 , volume = data.equalizerSettings.volume 207 208 } 208 209 209 - newPlaylists = 210 - { playlists 211 - | playlistToActivate = data.selectedPlaylist 212 - } 213 - 214 210 newQueue = 215 211 { queue 216 212 | repeat = data.repeat ··· 229 225 } 230 226 in 231 227 ( { model 232 - | eqSettings = newEqualizerSettings 233 - , playlists = newPlaylists 234 - , queue = newQueue 228 + | queue = newQueue 235 229 , tracks = newTracks 230 + 231 + -- 232 + , eqSettings = newEqualizerSettings 233 + , playlistToActivate = data.selectedPlaylist 236 234 } 237 235 -- 238 236 , Cmd.batch
+8 -8
src/Applications/UI/View.elm
··· 33 33 import UI.Navigation as Navigation 34 34 import UI.Notifications 35 35 import UI.Page as Page 36 - import UI.Playlists as Playlists 37 36 import UI.Playlists.ContextMenu as Playlists 37 + import UI.Playlists.View as Playlists 38 38 import UI.Queue as Queue 39 39 import UI.Queue.ContextMenu as Queue 40 40 import UI.Reply exposing (Reply(..)) ··· 182 182 nothing 183 183 184 184 Page.Playlists subPage -> 185 - model.extractedBackdropColor 186 - |> Lazy.lazy4 187 - Playlists.view 188 - subPage 189 - model.playlists 190 - model.tracks.selectedPlaylist 191 - |> Html.map PlaylistsMsg 185 + Lazy.lazy5 186 + Playlists.view 187 + subPage 188 + model.playlists 189 + model.tracks.selectedPlaylist 190 + model.editPlaylistContext 191 + model.extractedBackdropColor 192 192 193 193 Page.Queue subPage -> 194 194 model.queue