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

feat: Add collections

Changed files
+420 -121
src
+2 -1
CHANGELOG.md
··· 3 3 ## 3.5.0 4 4 5 5 - **Improve audio playback, processing and error handling**. 6 - - **No longer resets shuffle history when the collection or search changes**. 6 + - **Added collections, an alternative to playlists**. Tracks can only appear once in a collection and cannot be ordered. 7 7 - **Basic setup for themes**. 8 + - No longer resets shuffle history when the collection or search changes. 8 9 - WebDAV improvements. 9 10 - Removed Fission/Webnative user layer (discontinued). 10 11 - Minor improvements/fixes to the artwork downloading process.
+89 -20
src/Core/Themes/Sunrise/Playlists/View.elm
··· 46 46 Index -> 47 47 index playlists selectedPlaylist bgColor authMethodSupportsPublicData 48 48 49 - New -> 50 - new 49 + NewCollection -> 50 + newCollection 51 + 52 + NewPlaylist -> 53 + newPlaylist 51 54 ) 52 55 53 56 ··· 61 64 isSelected playlist = 62 65 Maybe.map (\s -> ( s.autoGenerated, s.name )) selectedPlaylist == Just ( playlist.autoGenerated, playlist.name ) 63 66 64 - customPlaylists = 67 + ( customCollections, customPlaylists ) = 65 68 playlists 66 69 |> List.filterNot (.autoGenerated >> Maybe.isJust) 67 70 |> List.sortBy lowercaseName 71 + |> List.partition .collection 68 72 69 73 customPlaylistListItem playlist = 70 74 if isSelected playlist then ··· 104 108 , isSelected = False 105 109 } 106 110 107 - directoryPlaylists = 111 + directoryCollections = 108 112 playlists 109 113 |> List.filter (.autoGenerated >> Maybe.isJust) 110 114 |> List.sortBy lowercaseName ··· 135 139 , NavigateToPage Page.Index 136 140 ) 137 141 , ( Icon Icons.add 142 + , Label "Create a new collection" Shown 143 + , NavigateToPage (Page.Playlists NewCollection) 144 + ) 145 + , ( Icon Icons.add 138 146 , Label "Create a new playlist" Shown 139 - , NavigateToPage (Page.Playlists New) 147 + , NavigateToPage (Page.Playlists NewPlaylist) 140 148 ) 141 149 ] 142 150 ··· 153 161 154 162 else 155 163 Kit.canister 156 - [ Kit.h1 "Playlists" 164 + [ Kit.h1 "Collections & Playlists" 157 165 158 166 -- Intro 159 167 -------- 160 168 , intro 161 169 170 + -- Custom Collections 171 + --------------------- 172 + , if List.isEmpty customCollections then 173 + nothing 174 + 175 + else 176 + raw 177 + [ category "Your Collections" 178 + , Themes.Sunrise.List.view 179 + Themes.Sunrise.List.Normal 180 + (List.map customPlaylistListItem customCollections) 181 + ] 182 + 162 183 -- Custom Playlists 163 184 ------------------- 164 185 , if List.isEmpty customPlaylists then ··· 172 193 (List.map customPlaylistListItem customPlaylists) 173 194 ] 174 195 175 - -- Directory Playlists 176 - ---------------------- 177 - , if List.isEmpty directoryPlaylists then 196 + -- Directory Collections 197 + ------------------------ 198 + , if List.isEmpty directoryCollections then 178 199 nothing 179 200 180 201 else 181 202 raw 182 - [ category "Autogenerated Directory Playlists" 203 + [ category "Autogenerated Directory Collections" 183 204 , Themes.Sunrise.List.view 184 205 Themes.Sunrise.List.Normal 185 - (List.map directoryPlaylistListItem directoryPlaylists) 206 + (List.map directoryPlaylistListItem directoryCollections) 186 207 ] 187 208 ] 188 209 ··· 191 212 Kit.centeredContent 192 213 [ slab 193 214 Html.a 194 - [ href (Page.toString <| Page.Playlists New) ] 215 + [ href (Page.toString <| Page.Playlists NewPlaylist) ] 195 216 [ "block" 196 217 , "opacity-30" 197 218 , "text-inherit" ··· 199 220 [ Icons.waves 64 Inherit ] 200 221 , slab 201 222 Html.a 202 - [ href (Page.toString <| Page.Playlists New) ] 223 + [ href (Page.toString <| Page.Playlists NewPlaylist) ] 203 224 [ "block" 204 225 , "leading-normal" 205 226 , "mt-2" ··· 209 230 ] 210 231 [ text "No playlists found, create one" 211 232 , lineBreak 212 - , text "or enable directory playlists." 233 + , text "or enable directory collections." 213 234 ] 214 235 ] 215 236 ··· 220 241 221 242 intro : Html Msg 222 243 intro = 223 - [ text "Playlists are not tied to the sources of its tracks, " 224 - , text "same goes for favourites." 244 + [ text "Collections and playlists are not tied to the sources of its tracks, " 245 + , text "same goes for favourites. " 225 246 , lineBreak 226 - , text "There's also directory playlists, which are playlists derived from root directories." 247 + , text "There's also directory collections, which are derived from root directories." 227 248 ] 228 249 |> raw 229 250 |> Kit.intro ··· 283 304 -- NEW 284 305 285 306 286 - new : List (Html Msg) 287 - new = 307 + newCollection : List (Html Msg) 308 + newCollection = 288 309 [ ----------------------------------------- 289 310 -- Navigation 290 311 ----------------------------------------- ··· 298 319 ----------------------------------------- 299 320 -- Content 300 321 ----------------------------------------- 301 - , [ Kit.h2 "Name your playlist" 322 + , [ Kit.h2 "Name your collection" 302 323 303 324 -- 304 325 , [ onInput SetPlaylistCreationContext 305 326 , placeholder "The Classics" 327 + ] 328 + |> Kit.textField 329 + |> chunky [ "max-w-md", "mx-auto" ] 330 + 331 + -- Button 332 + --------- 333 + , chunk 334 + [ "mt-10" ] 335 + [ Kit.button 336 + Normal 337 + Bypass 338 + (text "Create collection") 339 + ] 340 + ] 341 + |> Kit.canisterForm 342 + |> List.singleton 343 + |> Kit.centeredContent 344 + |> List.singleton 345 + |> slab 346 + Html.form 347 + [ onSubmit CreateCollection ] 348 + [ "flex" 349 + , "flex-grow" 350 + , "text-center" 351 + ] 352 + ] 353 + 354 + 355 + newPlaylist : List (Html Msg) 356 + newPlaylist = 357 + [ ----------------------------------------- 358 + -- Navigation 359 + ----------------------------------------- 360 + Navigation.local 361 + [ ( Icon Icons.arrow_back 362 + , Label "Back to list" Hidden 363 + , NavigateToPage (Page.Playlists Index) 364 + ) 365 + ] 366 + 367 + ----------------------------------------- 368 + -- Content 369 + ----------------------------------------- 370 + , [ Kit.h2 "Name your playlist" 371 + 372 + -- 373 + , [ onInput SetPlaylistCreationContext 374 + , placeholder "Sunset" 306 375 ] 307 376 |> Kit.textField 308 377 |> chunky [ "max-w-md", "mx-auto" ]
+5 -6
src/Core/Themes/Sunrise/Tracks/View.elm
··· 110 110 model.selectedPlaylist 111 111 |> Maybe.andThen 112 112 (\playlist -> 113 - case playlist.autoGenerated of 114 - Just _ -> 115 - Nothing 113 + if playlist.collection || Maybe.isJust playlist.autoGenerated then 114 + Nothing 116 115 117 - Nothing -> 118 - Just model.dnd 116 + else 117 + Just model.dnd 119 118 ) 120 119 |> Themes.Sunrise.Tracks.Scene.List.view 121 120 { bgColor = model.extractedBackdropColor ··· 364 363 Navigation.localWithTabindex 365 364 tabindex_ 366 365 [ ( Icon Icons.waves 367 - , Label "Playlists" Hidden 366 + , Label "Collections & Playlists" Hidden 368 367 , if pressedShift then 369 368 PerformMsg AssistWithSelectingPlaylist 370 369
+7 -1
src/Core/UI.elm
··· 427 427 AddTracksToPlaylist a -> 428 428 Playlists.addTracksToPlaylist a 429 429 430 + AssistWithAddingTracksToCollection a -> 431 + Playlists.assistWithAddingTracksToCollection a 432 + 430 433 AssistWithAddingTracksToPlaylist a -> 431 434 Playlists.assistWithAddingTracksToPlaylist a 432 435 433 436 AssistWithSelectingPlaylist -> 434 437 Playlists.assistWithSelectingPlaylist 435 438 439 + CreateCollection -> 440 + Playlists.createCollection 441 + 436 442 CreatePlaylist -> 437 - Playlists.create 443 + Playlists.createPlaylist 438 444 439 445 DeactivatePlaylist -> 440 446 Playlists.deactivate
+10
src/Core/UI/Commands/Alfred.elm
··· 96 96 97 97 -- 98 98 , { icon = Just (Icons.queue_music 16) 99 + , title = "Add current track to collection" 100 + , value = Command (UI.AssistWithAddingTracksToCollection <| [ identifiedTrack ]) 101 + } 102 + 103 + -- 104 + , { icon = Just (Icons.queue_music 16) 99 105 , title = "Add current track to playlist" 100 106 , value = Command (UI.AssistWithAddingTracksToPlaylist <| [ identifiedTrack ]) 101 107 } ··· 176 182 tracks -> 177 183 List.concat 178 184 [ [ { icon = Just (Icons.queue_music 16) 185 + , title = "Add current selection to collection" 186 + , value = Command (UI.AssistWithAddingTracksToCollection tracks) 187 + } 188 + , { icon = Just (Icons.queue_music 16) 179 189 , title = "Add current selection to playlist" 180 190 , value = Command (UI.AssistWithAddingTracksToPlaylist tracks) 181 191 }
+8 -3
src/Core/UI/Page.elm
··· 1 1 module UI.Page exposing (Page(..), fromUrl, rewriteUrl, sameBase, sources, toString) 2 2 3 + import Material.Icons exposing (collections) 3 4 import Maybe.Extra as Maybe 4 5 import Sources exposing (Service(..)) 5 6 import UI.Playlists.Page as Playlists ··· 78 79 Playlists Playlists.Index -> 79 80 "playlists" 80 81 81 - Playlists Playlists.New -> 82 - "playlists/new" 82 + Playlists Playlists.NewCollection -> 83 + "playlists/new/collection" 84 + 85 + Playlists Playlists.NewPlaylist -> 86 + "playlists/new/playlist" 83 87 84 88 Playlists (Playlists.Edit playlistName) -> 85 89 "playlists/edit/" ++ playlistName ··· 188 192 ----------------------------------------- 189 193 , map (Playlists Playlists.Index) (s "playlists") 190 194 , map (Playlists << Playlists.Edit) (s "playlists" </> s "edit" </> string) 191 - , map (Playlists Playlists.New) (s "playlists" </> s "new") 195 + , map (Playlists Playlists.NewCollection) (s "playlists" </> s "new" </> s "collection") 196 + , map (Playlists Playlists.NewPlaylist) (s "playlists" </> s "new" </> s "playlist") 192 197 193 198 ----------------------------------------- 194 199 -- Queue
+14 -9
src/Core/UI/Playlists/Alfred.elm
··· 15 15 -- CREATE 16 16 17 17 18 - create : List IdentifiedTrack -> List Playlist -> Alfred UI.Msg 19 - create tracks playlists = 18 + create : { collectionMode : Bool } -> List IdentifiedTrack -> List Playlist -> Alfred UI.Msg 19 + create { collectionMode } tracks playlists = 20 20 let 21 21 index = 22 22 makeIndex playlists 23 + 24 + subject = 25 + ifThenElse collectionMode "collection" "playlist" 23 26 in 24 27 Alfred.create 25 - { action = createAction tracks 28 + { action = createAction collectionMode tracks 26 29 , index = index 27 30 , message = 28 31 if List.length tracks == 1 then 29 - "Choose or create a playlist to add this track to." 32 + "Choose or create a " ++ subject ++ " to add this track to." 30 33 31 34 else 32 - "Choose or create a playlist to add these tracks to." 35 + "Choose or create a " ++ subject ++ " to add these tracks to." 33 36 , operation = QueryOrMutation 34 37 } 35 38 36 39 37 - createAction : List IdentifiedTrack -> Alfred.Action UI.Msg 38 - createAction tracks ctx = 40 + createAction : Bool -> List IdentifiedTrack -> Alfred.Action UI.Msg 41 + createAction collectionMode tracks ctx = 39 42 let 40 43 playlistTracks = 41 44 Tracks.toPlaylistTracks tracks ··· 47 50 case Alfred.stringValue result.value of 48 51 Just playlistName -> 49 52 [ UI.AddTracksToPlaylist 50 - { playlistName = playlistName 53 + { collection = collectionMode 54 + , playlistName = playlistName 51 55 , tracks = playlistTracks 52 56 } 53 57 ] ··· 62 66 case ctx.searchTerm of 63 67 Just searchTerm -> 64 68 [ UI.AddTracksToPlaylist 65 - { playlistName = searchTerm 69 + { collection = collectionMode 70 + , playlistName = searchTerm 66 71 , tracks = playlistTracks 67 72 } 68 73 ]
+5 -4
src/Core/UI/Playlists/ContextMenu.elm
··· 62 62 Just _ -> 63 63 ContextMenu 64 64 [ addToQueue identifiedTracksFromPlaylist 65 - , convertToRegularPlaylist tracksFromPlaylist playlist 65 + , convertToRegularCollection tracksFromPlaylist playlist 66 66 , downloadAsZip tracksFromPlaylist playlist 67 67 , storeInCache tracksFromPlaylist 68 68 ] ··· 97 97 } 98 98 99 99 100 - convertToRegularPlaylist tracksFromPlaylist playlist = 100 + convertToRegularCollection tracksFromPlaylist playlist = 101 101 Item 102 102 { icon = Icons.waves 103 - , label = "Convert to regular playlist" 103 + , label = "Save as regular collection" 104 104 , msg = 105 105 AddTracksToPlaylist 106 - { playlistName = playlist.name 106 + { collection = True 107 + , playlistName = playlist.name 107 108 , tracks = List.map Tracks.playlistTrackFromTrack tracksFromPlaylist 108 109 } 109 110
+5 -3
src/Core/UI/Playlists/Directory.elm
··· 1 1 module UI.Playlists.Directory exposing (generate) 2 2 3 3 import Dict exposing (Dict) 4 + import List.Extra as List 4 5 import Playlists exposing (..) 5 6 import Set exposing (Set) 6 7 import Sources exposing (Source) 7 8 import String.Ext as String 8 9 import Tracks exposing (Track) 9 - import List.Extra as List 10 + 10 11 11 12 12 13 -- 🔱 ··· 42 43 |> List.map 43 44 (\n -> 44 45 let 45 - s = 46 - String.split "/" n 46 + s = 47 + String.split "/" n 47 48 in 48 49 { autoGenerated = Just { level = List.length s - 1 } 50 + , collection = True 49 51 , name = Maybe.withDefault n (List.last s) 50 52 , public = False 51 53 , tracks = []
+2 -1
src/Core/UI/Playlists/Page.elm
··· 6 6 type Page 7 7 = Edit String 8 8 | Index 9 - | New 9 + | NewCollection 10 + | NewPlaylist
+173 -50
src/Core/UI/Playlists/State.elm
··· 33 33 |> andThen (Common.changeUrlUsingPage Page.Index) 34 34 35 35 36 - addTracksToPlaylist : { playlistName : String, tracks : List PlaylistTrack } -> Manager 37 - addTracksToPlaylist { playlistName, tracks } model = 36 + addTracksToPlaylist : { collection : Bool, playlistName : String, tracks : List PlaylistTrackWithoutMetadata } -> Manager 37 + addTracksToPlaylist { collection, playlistName, tracks } model = 38 38 let 39 39 properPlaylistName = 40 40 String.trim playlistName ··· 44 44 (\p -> Maybe.isNothing p.autoGenerated && p.name == properPlaylistName) 45 45 model.playlists 46 46 47 - newCollection = 47 + ( tracksAlreadyInPlaylist, newTracks ) = 48 + playlistIndex 49 + |> Maybe.andThen 50 + (\a -> 51 + if collection then 52 + Just a 53 + 54 + else 55 + Nothing 56 + ) 57 + |> Maybe.andThen (\idx -> List.getAt idx model.playlists) 58 + |> Maybe.map 59 + (\p -> 60 + List.foldl 61 + (\track ( a, b, c ) -> 62 + case 63 + List.findIndex 64 + (\x -> 65 + track.title == x.title && track.album == x.album && track.artist == x.artist 66 + ) 67 + c 68 + of 69 + Just idx -> 70 + ( track :: a, b, List.removeAt idx c ) 71 + 72 + Nothing -> 73 + ( a, track :: b, c ) 74 + ) 75 + ( [], [], p.tracks ) 76 + tracks 77 + ) 78 + |> Maybe.map (\( a, b, _ ) -> ( a, b )) 79 + |> Maybe.withDefault ( [], tracks ) 80 + |> Tuple.mapSecond 81 + (List.map 82 + (\track -> 83 + let 84 + newTrack : PlaylistTrack 85 + newTrack = 86 + { album = track.album 87 + , artist = track.artist 88 + , title = track.title 89 + 90 + -- 91 + , insertedAt = model.currentTime 92 + } 93 + in 94 + newTrack 95 + ) 96 + ) 97 + 98 + newInventory = 48 99 case playlistIndex of 49 100 Just idx -> 50 101 List.updateAt 51 102 idx 52 - (\p -> { p | tracks = p.tracks ++ tracks }) 103 + (\p -> { p | tracks = p.tracks ++ newTracks }) 53 104 model.playlists 54 105 55 106 Nothing -> 56 107 { autoGenerated = Nothing 108 + , collection = collection 57 109 , name = properPlaylistName 58 110 , public = False 59 - , tracks = tracks 111 + , tracks = newTracks 60 112 } 61 113 :: model.playlists 62 114 63 115 newModel = 64 116 { model 65 - | playlists = newCollection 66 - , lastModifiedPlaylist = Just properPlaylistName 117 + | playlists = newInventory 118 + , lastModifiedPlaylist = 119 + Just 120 + { collection = collection 121 + , name = properPlaylistName 122 + } 67 123 } 124 + 125 + subject = 126 + ifThenElse collection "collection" "playlist" 68 127 in 69 - (case tracks of 70 - [ t ] -> 71 - "Added __" ++ t.title ++ "__" 128 + case newTracks of 129 + [] -> 130 + if collection then 131 + (case tracksAlreadyInPlaylist of 132 + [ t ] -> 133 + "__" ++ t.title ++ "__ was" 72 134 73 - l -> 74 - "Added __" ++ String.fromInt (List.length l) ++ " tracks__" 75 - ) 76 - |> (\s -> s ++ " to the __" ++ properPlaylistName ++ "__ playlist") 77 - |> Notifications.success 78 - |> Common.showNotificationWithModel newModel 79 - |> andThen User.savePlaylists 135 + l -> 136 + "__" ++ String.fromInt (List.length l) ++ " tracks__ were" 137 + ) 138 + |> (\s -> s ++ " already added to the __" ++ properPlaylistName ++ "__ collection") 139 + |> Notifications.casual 140 + |> Common.showNotificationWithModel model 141 + 142 + else 143 + Return.singleton model 144 + 145 + _ -> 146 + (case newTracks of 147 + [ t ] -> 148 + "Added __" ++ t.title ++ "__" 149 + 150 + l -> 151 + "Added __" ++ String.fromInt (List.length l) ++ " tracks__" 152 + ) 153 + |> (\s -> s ++ " to the __" ++ properPlaylistName ++ "__ " ++ subject) 154 + |> Notifications.success 155 + |> Common.showNotificationWithModel newModel 156 + |> andThen User.savePlaylists 157 + 158 + 159 + assistWithAddingTracksToCollection : List IdentifiedTrack -> Manager 160 + assistWithAddingTracksToCollection tracks model = 161 + model.playlists 162 + |> List.filter (\p -> p.autoGenerated == Nothing && p.collection == True) 163 + |> UI.Playlists.Alfred.create { collectionMode = True } tracks 164 + |> (\a -> Alfred.assign a model) 80 165 81 166 82 167 assistWithAddingTracksToPlaylist : List IdentifiedTrack -> Manager 83 168 assistWithAddingTracksToPlaylist tracks model = 84 169 model.playlists 85 - |> List.filterNot (.autoGenerated >> Maybe.isJust) 86 - |> UI.Playlists.Alfred.create tracks 170 + |> List.filter (\p -> p.autoGenerated == Nothing && p.collection == False) 171 + |> UI.Playlists.Alfred.create { collectionMode = False } tracks 87 172 |> (\a -> Alfred.assign a model) 88 173 89 174 ··· 94 179 |> (\a -> Alfred.assign a model) 95 180 96 181 97 - create : Manager 98 - create model = 182 + create : { collection : Bool } -> Manager 183 + create { collection } model = 99 184 case model.newPlaylistContext of 100 185 Just playlistName -> 101 186 let 102 187 alreadyExists = 103 - List.any 188 + List.find 104 189 (.name >> (==) playlistName) 105 190 (List.filterNot (.autoGenerated >> Maybe.isJust) model.playlists) 106 191 107 192 playlist = 108 193 { autoGenerated = Nothing 194 + , collection = collection 109 195 , name = playlistName 110 196 , public = False 111 197 , tracks = [] 112 198 } 113 199 in 114 - if alreadyExists then 115 - "There's already a playlist with this name" 116 - |> Notifications.error 117 - |> Common.showNotificationWithModel model 200 + case alreadyExists of 201 + Just existingPlaylist -> 202 + (if existingPlaylist.collection then 203 + "There's already a collection using this name" 118 204 119 - else 120 - { model 121 - | lastModifiedPlaylist = Just playlist.name 122 - , newPlaylistContext = Nothing 123 - , playlists = playlist :: model.playlists 124 - } 125 - |> User.savePlaylists 126 - |> andThen redirectToPlaylistIndexPage 205 + else 206 + "There's already a playlist using this name" 207 + ) 208 + |> Notifications.error 209 + |> Common.showNotificationWithModel model 210 + 211 + Nothing -> 212 + { model 213 + | lastModifiedPlaylist = 214 + Just 215 + { collection = playlist.collection 216 + , name = playlist.name 217 + } 218 + , newPlaylistContext = Nothing 219 + , playlists = playlist :: model.playlists 220 + } 221 + |> User.savePlaylists 222 + |> andThen redirectToPlaylistIndexPage 127 223 128 224 Nothing -> 129 225 Return.singleton model 130 226 131 227 228 + createCollection : Manager 229 + createCollection = 230 + create { collection = True } 231 + 232 + 233 + createPlaylist : Manager 234 + createPlaylist = 235 + create { collection = False } 236 + 237 + 132 238 deactivate : Manager 133 239 deactivate = 134 240 deselect ··· 195 301 List.partition (.autoGenerated >> Maybe.isJust) model.playlists 196 302 197 303 alreadyExists = 198 - List.any 304 + List.find 199 305 (.name >> (==) properName) 200 306 notAutoGenerated 201 307 ··· 204 310 (\p -> ifThenElse (p.name == oldName) { p | name = properName } p) 205 311 notAutoGenerated 206 312 in 207 - if alreadyExists then 208 - "There's already a playlist with this name" 209 - |> Notifications.error 210 - |> Common.showNotificationWithModel 211 - { model | editPlaylistContext = Nothing } 313 + case alreadyExists of 314 + Just existingPlaylist -> 315 + (if existingPlaylist.collection then 316 + "There's already a collection using this name" 317 + 318 + else 319 + "There's already a playlist using this name" 320 + ) 321 + |> Notifications.error 322 + |> Common.showNotificationWithModel 323 + { model | editPlaylistContext = Nothing } 324 + 325 + Nothing -> 326 + if validName then 327 + { model 328 + | editPlaylistContext = Nothing 329 + , lastModifiedPlaylist = 330 + case model.lastModifiedPlaylist of 331 + Just l -> 332 + if l.name == oldName then 333 + Just { l | name = newName } 334 + 335 + else 336 + Just l 212 337 213 - else if validName then 214 - { model 215 - | editPlaylistContext = Nothing 216 - , lastModifiedPlaylist = Just properName 217 - , playlists = newCollection ++ autoGenerated 218 - } 219 - |> User.savePlaylists 220 - |> andThen redirectToPlaylistIndexPage 338 + Nothing -> 339 + Nothing 340 + , playlists = newCollection ++ autoGenerated 341 + } 342 + |> User.savePlaylists 343 + |> andThen redirectToPlaylistIndexPage 221 344 222 - else 223 - redirectToPlaylistIndexPage model 345 + else 346 + redirectToPlaylistIndexPage model 224 347 225 348 Nothing -> 226 349 redirectToPlaylistIndexPage model
+21
src/Core/UI/Queue/ContextMenu.elm
··· 48 48 } 49 49 , Item 50 50 { icon = Icons.waves 51 + , label = "Add to collection" 52 + , msg = AssistWithAddingTracksToCollection tracks 53 + , active = False 54 + } 55 + , Item 56 + { icon = Icons.waves 51 57 , label = "Add to playlist" 52 58 , msg = AssistWithAddingTracksToPlaylist tracks 53 59 , active = False ··· 76 82 else 77 83 [ Item 78 84 { icon = Icons.waves 85 + , label = "Add queue picks to collection" 86 + , msg = 87 + manualEntries 88 + |> List.map .identifiedTrack 89 + |> AssistWithAddingTracksToCollection 90 + , active = False 91 + } 92 + , Item 93 + { icon = Icons.waves 79 94 , label = "Add queue picks to playlist" 80 95 , msg = 81 96 manualEntries ··· 120 135 |> QueueMsg 121 136 122 137 -- 138 + , active = False 139 + } 140 + , Item 141 + { icon = Icons.waves 142 + , label = "Add to collection" 143 + , msg = AssistWithAddingTracksToCollection tracks 123 144 , active = False 124 145 } 125 146 , Item
+44 -12
src/Core/UI/Tracks/ContextMenu.elm
··· 23 23 { cached : List String 24 24 , cachingInProgress : List String 25 25 , currentTime : Time.Posix 26 - , lastModifiedPlaylistName : Maybe String 26 + , lastModifiedPlaylistName : Maybe { collection : Bool, name : String } 27 27 , selectedPlaylist : Maybe Playlist 28 28 , showAlternativeMenu : Bool 29 29 , sources : List Source ··· 152 152 153 153 playlistActions : 154 154 { selectedPlaylist : Maybe Playlist 155 - , lastModifiedPlaylistName : Maybe String 155 + , lastModifiedPlaylistName : Maybe { collection : Bool, name : String } 156 156 } 157 157 -> List IdentifiedTrack 158 158 -> List (ContextMenu.Item Msg) ··· 170 170 ) 171 171 selectedPlaylist 172 172 173 - maybeAddToLastModifiedPlaylist = 173 + maybeAddToLastModifiedPlaylist { collection } = 174 174 Maybe.andThen 175 - (\n -> 176 - if Maybe.map .name selectedPlaylist /= Just n then 175 + (\l -> 176 + if Maybe.map .name selectedPlaylist /= Just l.name && l.collection == collection then 177 177 justAnItem 178 178 { icon = Icons.waves 179 - , label = "Add to \"" ++ n ++ "\"" 179 + , label = "Add to \"" ++ l.name ++ "\"" 180 180 , msg = 181 181 AddTracksToPlaylist 182 - { playlistName = n 182 + { collection = collection 183 + , playlistName = l.name 183 184 , tracks = Tracks.toPlaylistTracks tracks 184 185 } 185 186 ··· 198 199 ----------------------------------------- 199 200 Just playlist -> 200 201 Maybe.values 201 - [ justAnItem 202 + [ maybeAddToLastModifiedPlaylist { collection = True } 203 + , justAnItem 202 204 { icon = Icons.waves 203 - , label = "Remove from playlist" 205 + , label = 206 + if playlist.collection then 207 + "Remove from collection" 208 + 209 + else 210 + "Remove from playlist" 204 211 , msg = RemoveTracksFromPlaylist playlist tracks 205 212 206 213 -- 207 214 , active = False 208 215 } 209 - , maybeAddToLastModifiedPlaylist 210 216 , justAnItem 211 217 { icon = Icons.waves 212 - , label = "Add to another playlist" 218 + , label = 219 + if playlist.collection then 220 + "Add to another collection" 221 + 222 + else 223 + "Add to playlist" 224 + , msg = AssistWithAddingTracksToCollection tracks 225 + 226 + -- 227 + , active = False 228 + } 229 + , maybeAddToLastModifiedPlaylist { collection = False } 230 + , justAnItem 231 + { icon = Icons.waves 232 + , label = 233 + if playlist.collection then 234 + "Add to playlist" 235 + 236 + else 237 + "Add to another playlist" 213 238 , msg = AssistWithAddingTracksToPlaylist tracks 214 239 215 240 -- ··· 222 247 ----------------------------------------- 223 248 _ -> 224 249 Maybe.values 225 - [ maybeAddToLastModifiedPlaylist 250 + [ maybeAddToLastModifiedPlaylist { collection = True } 251 + , justAnItem 252 + { icon = Icons.waves 253 + , label = "Add to collection" 254 + , msg = AssistWithAddingTracksToCollection tracks 255 + , active = False 256 + } 257 + , maybeAddToLastModifiedPlaylist { collection = False } 226 258 , justAnItem 227 259 { icon = Icons.waves 228 260 , label = "Add to playlist"
+5 -3
src/Core/UI/Types.elm
··· 20 20 import LastFm 21 21 import Management 22 22 import Notifications exposing (Notification) 23 - import Playlists exposing (Playlist, PlaylistTrack) 23 + import Playlists exposing (Playlist, PlaylistTrack, PlaylistTrackWithoutMetadata) 24 24 import Queue 25 25 import Random 26 26 import Sources exposing (Source) ··· 124 124 -- Playlists 125 125 ----------------------------------------- 126 126 , editPlaylistContext : Maybe { oldName : String, newName : String } 127 - , lastModifiedPlaylist : Maybe String 127 + , lastModifiedPlaylist : Maybe { collection : Bool, name : String } 128 128 , newPlaylistContext : Maybe String 129 129 , playlists : List Playlist 130 130 , playlistToActivate : Maybe String ··· 256 256 -- Playlists 257 257 ----------------------------------------- 258 258 | ActivatePlaylist Playlist 259 - | AddTracksToPlaylist { playlistName : String, tracks : List PlaylistTrack } 259 + | AddTracksToPlaylist { collection : Bool, playlistName : String, tracks : List PlaylistTrackWithoutMetadata } 260 + | AssistWithAddingTracksToCollection (List IdentifiedTrack) 260 261 | AssistWithAddingTracksToPlaylist (List IdentifiedTrack) 261 262 | AssistWithSelectingPlaylist 263 + | CreateCollection 262 264 | CreatePlaylist 263 265 | DeactivatePlaylist 264 266 | DeletePlaylist { playlistName : String }
+15
src/Library/Playlists.elm
··· 1 1 module Playlists exposing (..) 2 2 3 + import Time 4 + 5 + 6 + 3 7 -- 🌳 4 8 5 9 6 10 type alias Playlist = 7 11 { autoGenerated : Maybe { level : Int } 12 + , collection : Bool 8 13 , name : String 9 14 , public : Bool 10 15 , tracks : List PlaylistTrack 11 16 } 12 17 13 18 19 + type alias PlaylistTrackWithoutMetadata = 20 + { album : Maybe String 21 + , artist : Maybe String 22 + , title : String 23 + } 24 + 25 + 14 26 type alias PlaylistTrack = 15 27 { album : Maybe String 16 28 , artist : Maybe String 17 29 , title : String 30 + 31 + -- 32 + , insertedAt : Time.Posix 18 33 } 19 34 20 35
+10 -3
src/Library/Playlists/Encoding.elm
··· 4 4 import Json.Decode.Ext as Decode 5 5 import Json.Encode as Encode 6 6 import Json.Encode.Ext exposing (..) 7 - import Maybe.Extra as Maybe exposing (isJust) 7 + import Maybe.Extra as Maybe 8 8 import Playlists exposing (..) 9 + import Time.Ext as Time 9 10 10 11 11 12 ··· 24 25 Nothing -> 25 26 Encode.null 26 27 ) 28 + , ( "collection", Encode.bool playlist.collection ) 27 29 , ( "name", Encode.string playlist.name ) 28 30 , ( "public", Encode.bool playlist.public ) 29 31 , ( "tracks", Encode.list encodePlaylistTrack playlist.tracks ) ··· 36 38 [ ( "album", encodeMaybe playlistTrack.album Encode.string ) 37 39 , ( "artist", encodeMaybe playlistTrack.artist Encode.string ) 38 40 , ( "title", Encode.string playlistTrack.title ) 41 + 42 + -- 43 + , ( "insertAt", Time.encode playlistTrack.insertedAt ) 39 44 ] 40 45 41 46 ··· 45 50 46 51 decoder : Decode.Decoder Playlist 47 52 decoder = 48 - Decode.map4 Playlist 53 + Decode.map5 Playlist 49 54 (Decode.field "autoGenerated" <| Decode.maybe <| Decode.map (\l -> { level = l }) <| Decode.field "level" Decode.int) 55 + (Decode.optionalField "collection" Decode.bool False) 50 56 (Decode.field "name" Decode.string) 51 57 (Decode.optionalField "public" Decode.bool False) 52 58 (Decode.field "tracks" <| Decode.list playlistTrackDecoder) ··· 54 60 55 61 playlistTrackDecoder : Decode.Decoder PlaylistTrack 56 62 playlistTrackDecoder = 57 - Decode.map3 PlaylistTrack 63 + Decode.map4 PlaylistTrack 58 64 (Decode.maybe <| Decode.field "album" Decode.string) 59 65 (Decode.maybe <| Decode.field "artist" Decode.string) 60 66 (Decode.field "title" Decode.string) 67 + (Decode.optionalField "insertedAt" Time.decoder Time.default)
+2 -2
src/Library/Playlists/Matching.elm
··· 13 13 List.foldl 14 14 (\( i, t ) ( identifiedTracks, remainingPlaylistTracks ) -> 15 15 let 16 - imaginaryPlaylistTrack = 16 + im = 17 17 { album = t.tags.album 18 18 , artist = t.tags.artist 19 19 , title = t.tags.title ··· 22 22 ( matches, remainingPlaylistTracksWithoutMatches ) = 23 23 List.foldl 24 24 (\( pi, pt ) -> 25 - if imaginaryPlaylistTrack == pt then 25 + if im.title == pt.title && im.album == pt.album && im.artist == pt.artist then 26 26 Tuple.mapBoth 27 27 ((::) ( playlistTrackIdentifiers i pi, t )) 28 28 identity
+3 -3
src/Library/Tracks.elm
··· 3 3 import Base64 4 4 import List.Extra as List 5 5 import Maybe.Extra as Maybe 6 - import Playlists exposing (Playlist, PlaylistTrack) 6 + import Playlists exposing (Playlist, PlaylistTrack, PlaylistTrackWithoutMetadata) 7 7 import String.Ext as String 8 8 import Time 9 9 import Time.Ext as Time ··· 442 442 |> Maybe.withDefault False 443 443 444 444 445 - playlistTrackFromTrack : Track -> PlaylistTrack 445 + playlistTrackFromTrack : Track -> PlaylistTrackWithoutMetadata 446 446 playlistTrackFromTrack track = 447 447 { album = track.tags.album 448 448 , artist = track.tags.artist ··· 450 450 } 451 451 452 452 453 - toPlaylistTracks : List IdentifiedTrack -> List PlaylistTrack 453 + toPlaylistTracks : List IdentifiedTrack -> List PlaylistTrackWithoutMetadata 454 454 toPlaylistTracks = 455 455 List.map (Tuple.second >> playlistTrackFromTrack) 456 456