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

Configure Feed

Select the types of activity you want to include in your feed.

Add command palette (#323)

* Use control key modifier for keyboard shortcuts

* Update shortcuts on about page

* Improve command palette design and add icons

* Further tweak alfred/contextmenu/overlay styling

* Implement simple command palette

* Command groups

* Add a bunch of new commands

authored by

Steven Vandevelde and committed by
GitHub
4f41fa07 14d78835

+937 -141
+3
src/Applications/UI.elm
··· 432 432 LinkClicked a -> 433 433 Routing.linkClicked a 434 434 435 + OpenUrlOnNewPage a -> 436 + Routing.openUrlOnNewPage a 437 + 435 438 PageChanged a -> 436 439 Routing.transition a 437 440
+25 -15
src/Applications/UI/Adjunct.elm
··· 6 6 import UI.Alfred.State as Alfred 7 7 import UI.Audio.State as Audio 8 8 import UI.Authentication.Common as Authentication 9 + import UI.Commands.State as Commands 9 10 import UI.Common.State as Common 10 11 import UI.Interface.State exposing (hideOverlay) 11 12 import UI.Page as Page ··· 60 61 61 62 else 62 63 case m.pressedKeys of 63 - [ Keyboard.Character "{", Keyboard.Shift ] -> 64 + [ Keyboard.Character "[", Keyboard.Control ] -> 64 65 Queue.rewind m 65 66 66 - [ Keyboard.Character "}", Keyboard.Shift ] -> 67 + [ Keyboard.Character "]", Keyboard.Control ] -> 67 68 Queue.shift m 68 69 69 - [ Keyboard.Character "<", Keyboard.Shift ] -> 70 + [ Keyboard.Character "{", Keyboard.Shift, Keyboard.Control ] -> 70 71 Audio.seek ((m.audioPosition - 10) / m.audioDuration) m 71 72 72 - [ Keyboard.Character ">", Keyboard.Shift ] -> 73 + [ Keyboard.Character "}", Keyboard.Shift, Keyboard.Control ] -> 73 74 Audio.seek ((m.audioPosition + 10) / m.audioDuration) m 74 75 76 + -- Meta key 75 77 -- 76 - [ Keyboard.Character "L" ] -> 78 + [ Keyboard.Character "K", Keyboard.Meta ] -> 79 + Commands.showPalette m 80 + 81 + -- Ctrl key 82 + -- 83 + [ Keyboard.Character "K", Keyboard.Control ] -> 84 + Commands.showPalette m 85 + 86 + [ Keyboard.Character "L", Keyboard.Control ] -> 77 87 Playlists.assistWithSelectingPlaylist m 78 88 79 - [ Keyboard.Character "N" ] -> 89 + [ Keyboard.Character "N", Keyboard.Control ] -> 80 90 Tracks.scrollToNowPlaying m 81 91 82 - [ Keyboard.Character "P" ] -> 92 + [ Keyboard.Character "P", Keyboard.Control ] -> 83 93 Audio.playPause m 84 94 85 - [ Keyboard.Character "R" ] -> 95 + [ Keyboard.Character "R", Keyboard.Control ] -> 86 96 Queue.toggleRepeat m 87 97 88 - [ Keyboard.Character "S" ] -> 98 + [ Keyboard.Character "S", Keyboard.Control ] -> 89 99 Queue.toggleShuffle m 90 100 91 101 -- 92 - [ Keyboard.Character "1" ] -> 102 + [ Keyboard.Character "1", Keyboard.Control ] -> 93 103 Common.changeUrlUsingPage Page.Index m 94 104 95 - [ Keyboard.Character "2" ] -> 105 + [ Keyboard.Character "2", Keyboard.Control ] -> 96 106 Common.changeUrlUsingPage (Page.Playlists Playlists.Index) m 97 107 98 - [ Keyboard.Character "3" ] -> 108 + [ Keyboard.Character "3", Keyboard.Control ] -> 99 109 Common.changeUrlUsingPage (Page.Queue Queue.Index) m 100 110 101 - [ Keyboard.Character "4" ] -> 111 + [ Keyboard.Character "4", Keyboard.Control ] -> 102 112 Common.changeUrlUsingPage Page.Equalizer m 103 113 104 - [ Keyboard.Character "8" ] -> 114 + [ Keyboard.Character "8", Keyboard.Control ] -> 105 115 Common.changeUrlUsingPage (Page.Sources Sources.Index) m 106 116 107 - [ Keyboard.Character "9" ] -> 117 + [ Keyboard.Character "9", Keyboard.Control ] -> 108 118 Common.changeUrlUsingPage (Page.Settings Settings.Index) m 109 119 110 120 --
+20 -6
src/Applications/UI/Alfred/State.elm
··· 20 20 |> Process.sleep 21 21 |> Task.andThen (\_ -> Dom.focus "diffuse__alfred") 22 22 |> Task.attempt (\_ -> UI.Bypass) 23 - |> return { model | alfred = Just instance } 23 + -- The "K" key seems to stick when using CMD + K, 24 + -- aka. Meta key + K, to show the command palette. 25 + -- https://github.com/ohanhi/keyboard/issues/14 26 + |> return { model | alfred = Just instance, pressedKeys = [] } 24 27 25 28 26 29 gotInput : String -> Manager ··· 34 37 runAction index model = 35 38 case model.alfred of 36 39 Just instance -> 37 - { result = List.getAt index instance.results 40 + { result = Alfred.getAt index instance 38 41 , searchTerm = instance.searchTerm 39 42 } 40 43 |> instance.action ··· 78 81 selectNext model = 79 82 case model.alfred of 80 83 Just instance -> 84 + let 85 + total = 86 + Alfred.length instance 87 + in 81 88 instance 82 - |> (\i -> { i | focus = min (i.focus + 1) (List.length i.results - 1) }) 89 + |> (\i -> { i | focus = min (i.focus + 1) (total - 1) }) 83 90 |> (\i -> { model | alfred = Just i }) 84 91 |> scrollToFocus 85 92 ··· 118 125 , searchTerm = 119 126 Just searchTerm 120 127 , results = 121 - alfred.index 122 - |> List.filter (String.toLower >> String.contains lowerSearchTerm) 123 - |> List.sort 128 + List.map 129 + (\group -> 130 + group.items 131 + |> List.filter 132 + (.title >> String.toLower >> String.contains lowerSearchTerm) 133 + |> (\items -> 134 + { group | items = items } 135 + ) 136 + ) 137 + alfred.index 124 138 } 125 139 126 140 else
+130 -63
src/Applications/UI/Alfred/View.elm
··· 43 43 [ "italic" 44 44 , "leading-normal" 45 45 , "mt-12" 46 + , "opacity-75" 46 47 , "text-center" 47 48 , "text-white" 48 49 ··· 66 67 ) 67 68 ] 68 69 [ "text-sm" 69 - , "max-w-lg" 70 + , "max-w-xl" 70 71 , "mt-8" 71 72 , "w-full" 72 73 ] ··· 88 89 Mutation -> 89 90 placeholder "Type to create" 90 91 ] 91 - [ "border-none" 92 + [ "border" 92 93 , "bg-white" 93 94 , "block" 94 95 , "leading-normal" 95 - , "rounded" 96 + , "opacity-95" 96 97 , "outline-none" 97 98 , "p-4" 99 + , "rounded-t" 98 100 , "shadow-md" 99 - , "text-2xl" 101 + , "text-xl" 100 102 , "tracking-tad-closer" 101 103 , "w-full" 102 104 103 105 -- Dark mode 104 106 ------------ 105 - , "dark:bg-base00" 107 + , "dark:bg-darkest-hour" 108 + , "dark:border-base00" 106 109 ] 107 110 [] 108 111 ] ··· 113 116 , brick 114 117 [ id "alfred__results" ] 115 118 [ "bg-white" 116 - , "rounded" 117 - , "leading-none" 118 - , "max-w-lg" 119 + , "border" 120 + , "border-t-0" 121 + , "leading-tight" 122 + , "max-w-xl" 119 123 , "mb-32" 120 - , "mt-8" 124 + , "opacity-95" 121 125 , "overflow-x-hidden" 122 126 , "overflow-y-auto" 127 + , "rounded-b" 123 128 , "shadow-md" 124 129 , "smooth-scrolling" 125 130 , "text-nearly-sm" ··· 127 132 128 133 -- Dark mode 129 134 ------------ 130 - , "dark:bg-base00" 135 + , "dark:bg-darkest-hour" 136 + , "dark:border-base00" 137 + ] 138 + (instance.results 139 + |> List.foldl 140 + (\group ( acc, indexBase ) -> 141 + case List.length group.items of 142 + 0 -> 143 + ( acc, indexBase ) 144 + 145 + x -> 146 + ( groupView bgColor instance group indexBase :: acc 147 + , indexBase + x 148 + ) 149 + ) 150 + ( [], 0 ) 151 + |> Tuple.first 152 + |> List.reverse 153 + ) 154 + ] 155 + 156 + Nothing -> 157 + nothing 158 + 159 + 160 + groupView bgColor instance group indexBase = 161 + raw 162 + [ case group.name of 163 + Just name -> 164 + chunk 165 + [ "all-small-caps" 166 + , "antialiased" 167 + , "font-semibold" 168 + , "leading-tight" 169 + , "mb-2" 170 + , "mx-2" 171 + , "mt-5" 172 + , "opacity-60" 173 + , "px-3" 174 + , "text-sm" 175 + , "tracking-wider" 131 176 ] 132 - (List.indexedMap 133 - (\idx result -> 134 - brick 135 - [ onTapPreventDefault (UI.SelectAlfredItem idx) 177 + [ Html.text name ] 136 178 137 - -- 138 - , if idx == instance.focus then 139 - id "alfred__results__focus" 179 + Nothing -> 180 + Html.text "" 181 + , raw 182 + (List.indexedMap 183 + (\i -> itemView bgColor instance <| indexBase + i) 184 + group.items 185 + ) 186 + ] 140 187 141 - else 142 - id ("alfred__results__" ++ String.fromInt idx) 143 188 144 - -- 145 - , if idx == instance.focus then 146 - style "background-color" bgColor 189 + itemView bgColor instance idx item = 190 + brick 191 + [ onTapPreventDefault (UI.SelectAlfredItem idx) 192 + 193 + -- 194 + , if idx == instance.focus then 195 + id "alfred__results__focus" 196 + 197 + else 198 + id ("alfred__results__" ++ String.fromInt idx) 199 + 200 + -- 201 + , if idx == instance.focus then 202 + style "background-color" bgColor 147 203 148 - else 149 - style "" "" 150 - ] 151 - (List.concat 152 - [ [ "p-4" 153 - , "relative" 154 - , "truncate" 155 - ] 204 + else 205 + style "" "" 206 + ] 207 + (List.concat 208 + [ [ "flex" 209 + , "items-center" 210 + , "m-2" 211 + , "p-3" 212 + , "relative" 213 + , "rounded" 214 + ] 156 215 157 - -- 158 - , if idx == instance.focus then 159 - [ "text-white", "dark:text-base07" ] 216 + -- 217 + , if idx == instance.focus then 218 + [ "text-white" 219 + , "dark:opacity-80" 220 + , "dark:text-base07" 221 + ] 160 222 161 - else 162 - [ "text-inherit" ] 223 + else 224 + [ "text-inherit" ] 163 225 164 - -- 165 - , if modBy 2 idx == 0 then 166 - [] 226 + -- 227 + -- , if modBy 2 idx == 0 then 228 + -- [] 229 + -- else 230 + -- [ "bg-gray-100", "dark:bg-base01-15" ] 231 + ] 232 + ) 233 + [ case item.icon of 234 + Just icon -> 235 + slab 236 + Html.span 237 + [] 238 + [ "inline-block" 239 + , "mr-2" 240 + , "w-5" 241 + ] 242 + [ icon Inherit 243 + ] 167 244 168 - else 169 - [ "bg-gray-100", "dark:bg-base01-15" ] 170 - ] 171 - ) 172 - [ text result 245 + Nothing -> 246 + text "" 173 247 174 - -- 175 - , if idx == instance.focus then 176 - chunk 177 - [ "absolute" 178 - , "leading-0" 179 - , "-translate-y-1/2" 180 - , "mr-3" 181 - , "right-0" 182 - , "top-1/2" 183 - , "transform" 184 - ] 185 - [ Icons.keyboard_return 13 Inherit 186 - ] 248 + -- 249 + , slab 250 + Html.span 251 + [] 252 + [ "flex-1", "inline-block", "pt-px" ] 253 + [ text item.title ] 187 254 188 - else 189 - nothing 190 - ] 191 - ) 192 - instance.results 193 - ) 255 + -- 256 + , if idx == instance.focus then 257 + chunk 258 + [ "leading-0", "ml-2" ] 259 + [ Icons.keyboard_return 13 Inherit 194 260 ] 195 261 196 - Nothing -> 262 + else 197 263 nothing 264 + ]
-1
src/Applications/UI/Authentication/View.elm
··· 222 222 ] 223 223 [ "align-middle" 224 224 , "inline-block" 225 - , "pt-px" 226 225 , "text-nearly-sm" 227 226 ] 228 227 [ text "SIGN IN" ]
+357
src/Applications/UI/Commands/Alfred.elm
··· 1 + module UI.Commands.Alfred exposing (commands, palette) 2 + 3 + import Alfred exposing (..) 4 + import Conditional exposing (ifThenElse) 5 + import List.Extra as List 6 + import Material.Icons as Icons 7 + import Tracks exposing (Grouping(..), SortBy(..)) 8 + import UI.Page as Page 9 + import UI.Queue.Types as Queue 10 + import UI.Sources.Page as Sources 11 + import UI.Sources.Types as Sources 12 + import UI.Tracks.Types as Tracks 13 + import UI.Types as UI 14 + 15 + 16 + palette : UI.Model -> Alfred UI.Msg 17 + palette model = 18 + Alfred.create 19 + { action = action 20 + , index = commands model 21 + , message = "Run a command." 22 + , operation = Query 23 + } 24 + 25 + 26 + 27 + -- ⛰ 28 + 29 + 30 + commands : UI.Model -> List (Alfred.Group UI.Msg) 31 + commands model = 32 + [ { name = Just "Currently playing", items = nowPlayingCommands model } 33 + , { name = Just "Track selection", items = selectionCommands model } 34 + , { name = Just "View", items = viewCommands model } 35 + , { name = Just "Playback", items = playbackCommands model } 36 + , { name = Just "Sources", items = sourcesCommands model } 37 + , { name = Just "Data", items = dataCommands model } 38 + , { name = Just "Misc", items = miscCommands model } 39 + ] 40 + 41 + 42 + 43 + -- 44 + 45 + 46 + dataCommands model = 47 + [ { icon = Just (Icons.offline_bolt 16) 48 + , title = "Clear tracks cache" 49 + , value = Command (UI.TracksMsg Tracks.ClearCache) 50 + } 51 + , { icon = Just (Icons.save 16) 52 + , title = "Export data" 53 + , value = Command UI.Export 54 + } 55 + , { icon = Just (Icons.save 16) 56 + , title = "Import data (⚠️ will override current data)" 57 + , value = Command UI.RequestImport 58 + } 59 + , { icon = Just (Icons.save 16) 60 + , title = "Migrate user data to different storage" 61 + , value = Command UI.MigrateHypaethralUserData 62 + } 63 + ] 64 + 65 + 66 + miscCommands model = 67 + [ { icon = Just (Icons.help 16) 68 + , title = "Open help" 69 + , value = Command (UI.OpenUrlOnNewPage "./about/#UI") 70 + } 71 + ] 72 + 73 + 74 + nowPlayingCommands : UI.Model -> List (Item UI.Msg) 75 + nowPlayingCommands model = 76 + case model.nowPlaying of 77 + Just queueItem -> 78 + let 79 + ( queueItemIdentifiers, _ ) = 80 + queueItem.identifiedTrack 81 + 82 + identifiedTrack = 83 + model.tracks.harvested 84 + |> List.getAt queueItemIdentifiers.indexInList 85 + |> Maybe.withDefault queueItem.identifiedTrack 86 + 87 + ( identifiers, track ) = 88 + identifiedTrack 89 + in 90 + [ { icon = Just (Icons.search 16) 91 + , title = "Show current track in list" 92 + , value = Command (UI.TracksMsg Tracks.ScrollToNowPlaying) 93 + } 94 + 95 + -- 96 + , { icon = Just (Icons.favorite 14) 97 + , title = ifThenElse identifiers.isFavourite "Remove favourite" "Mark as favourite" 98 + , value = Command (UI.TracksMsg <| Tracks.ToggleFavourite identifiers.indexInList) 99 + } 100 + 101 + -- 102 + , { icon = Just (Icons.queue_music 16) 103 + , title = "Add current track to playlist" 104 + , value = Command (UI.AssistWithAddingTracksToPlaylist <| [ identifiedTrack ]) 105 + } 106 + 107 + -- 108 + , { icon = Just (Icons.offline_bolt 16) 109 + , title = "Add current track to cache" 110 + , value = 111 + [ track ] 112 + |> Tracks.StoreInCache 113 + |> UI.TracksMsg 114 + |> Command 115 + } 116 + ] 117 + 118 + Nothing -> 119 + [] 120 + 121 + 122 + playbackCommands model = 123 + [ if model.audioIsPlaying then 124 + { icon = Just (Icons.pause 16) 125 + , title = "Pause" 126 + , value = Command UI.TogglePlay 127 + } 128 + 129 + else 130 + { icon = Just (Icons.play_arrow 16) 131 + , title = "Play" 132 + , value = Command UI.TogglePlay 133 + } 134 + 135 + -- 136 + , { icon = Just (Icons.fast_rewind 18) 137 + , title = "Previous track" 138 + , value = Command (UI.QueueMsg Queue.Rewind) 139 + } 140 + , { icon = Just (Icons.fast_forward 18) 141 + , title = "Next track" 142 + , value = Command (UI.QueueMsg Queue.Shift) 143 + } 144 + , { icon = Just (Icons.repeat 16) 145 + , title = toggle model.repeat "repeat" 146 + , value = Command (UI.QueueMsg Queue.ToggleRepeat) 147 + } 148 + , { icon = Just (Icons.shuffle 16) 149 + , title = toggle model.shuffle "shuffle" 150 + , value = Command (UI.QueueMsg Queue.ToggleShuffle) 151 + } 152 + ] 153 + 154 + 155 + selectionCommands model = 156 + let 157 + ( selection, _, amountOfFavs ) = 158 + List.foldr 159 + (\( i, t ) ( acc, selected, favouriteCounter ) -> 160 + case List.findIndex ((==) i.indexInList) selected of 161 + Just s -> 162 + ( ( i, t ) :: acc 163 + , List.removeAt s selected 164 + , ifThenElse i.isFavourite (favouriteCounter + 1) favouriteCounter 165 + ) 166 + 167 + Nothing -> 168 + ( acc, selected, favouriteCounter ) 169 + ) 170 + ( [] 171 + , model.selectedTrackIndexes 172 + , 0 173 + ) 174 + model.tracks.harvested 175 + in 176 + case selection of 177 + [] -> 178 + [] 179 + 180 + tracks -> 181 + List.concat 182 + [ [ { icon = Just (Icons.queue_music 16) 183 + , title = "Add current selection to playlist" 184 + , value = Command (UI.AssistWithAddingTracksToPlaylist tracks) 185 + } 186 + ] 187 + 188 + -- 189 + , if amountOfFavs > 0 then 190 + [ { icon = Just (Icons.favorite 14) 191 + , title = "Remove current selection from favourites" 192 + , value = Command (UI.TracksMsg <| Tracks.RemoveFavourites tracks) 193 + } 194 + ] 195 + 196 + else 197 + [] 198 + 199 + -- 200 + , if amountOfFavs < List.length selection then 201 + [ { icon = Just (Icons.favorite 14) 202 + , title = "Add current selection to favourites" 203 + , value = Command (UI.TracksMsg <| Tracks.AddFavourites tracks) 204 + } 205 + ] 206 + 207 + else 208 + [] 209 + ] 210 + 211 + 212 + sourcesCommands model = 213 + [ { icon = Just (Icons.sync 16) 214 + , title = "Process sources" 215 + , value = Command (UI.SourcesMsg Sources.Process) 216 + } 217 + 218 + -- 219 + , { icon = Just (Icons.add 16) 220 + , title = "Add new source" 221 + , value = Command (UI.ChangeUrlUsingPage <| Page.Sources Sources.New) 222 + } 223 + ] 224 + 225 + 226 + viewCommands model = 227 + let 228 + sortCommands = 229 + (case Maybe.map .autoGenerated model.selectedPlaylist of 230 + Just False -> 231 + [] 232 + 233 + _ -> 234 + case model.scene of 235 + Tracks.Covers -> 236 + [ Album, Artist ] 237 + 238 + Tracks.List -> 239 + [ Album, Artist, Title ] 240 + ) 241 + |> List.remove 242 + model.sortBy 243 + |> List.map 244 + (\sortBy -> 245 + { icon = 246 + Just (Icons.sort 16) 247 + , title = 248 + case sortBy of 249 + Artist -> 250 + "Sort tracks by artist" 251 + 252 + Album -> 253 + "Sort tracks by album" 254 + 255 + PlaylistIndex -> 256 + "Sort tracks by playlist index" 257 + 258 + Title -> 259 + "Sort tracks by title" 260 + , value = 261 + Command (UI.TracksMsg <| Tracks.SortBy sortBy) 262 + } 263 + ) 264 + 265 + groupCommands = 266 + [ AddedOn, Directory, FirstAlphaCharacter, TrackYear ] 267 + |> (case model.grouping of 268 + Just group -> 269 + List.remove group 270 + 271 + Nothing -> 272 + identity 273 + ) 274 + |> List.map 275 + (\group -> 276 + { icon = 277 + Just (Icons.library_music 16) 278 + , title = 279 + case group of 280 + AddedOn -> 281 + "Group by processing date" 282 + 283 + Directory -> 284 + "Group by directory" 285 + 286 + FirstAlphaCharacter -> 287 + "Group by first letter" 288 + 289 + TrackYear -> 290 + "Group by track year" 291 + , value = 292 + Command (UI.TracksMsg <| Tracks.GroupBy group) 293 + } 294 + ) 295 + |> (\list -> 296 + case model.grouping of 297 + Just _ -> 298 + { icon = Just (Icons.library_music 16) 299 + , title = "Disable grouping" 300 + , value = Command (UI.TracksMsg Tracks.DisableGrouping) 301 + } 302 + :: list 303 + 304 + Nothing -> 305 + list 306 + ) 307 + in 308 + [ { icon = Just (Icons.favorite 14) 309 + , title = toggle model.favouritesOnly "favourites-only mode" 310 + , value = Command (UI.TracksMsg Tracks.ToggleFavouritesOnly) 311 + } 312 + 313 + -- 314 + , case model.scene of 315 + Tracks.Covers -> 316 + { icon = Just (Icons.notes 16) 317 + , title = "Switch to list view" 318 + , value = Command (UI.TracksMsg <| Tracks.ChangeScene Tracks.List) 319 + } 320 + 321 + Tracks.List -> 322 + { icon = Just (Icons.burst_mode 18) 323 + , title = "Switch to cover view" 324 + , value = Command (UI.TracksMsg <| Tracks.ChangeScene Tracks.Covers) 325 + } 326 + 327 + -- 328 + , { icon = Just (Icons.filter_list 16) 329 + , title = ifThenElse model.cachedTracksOnly "Disable cached-tracks-only mode" "Only show cached tracks" 330 + , value = Command (UI.TracksMsg Tracks.ToggleCachedOnly) 331 + } 332 + 333 + -- 334 + , { icon = Just (Icons.sort 16) 335 + , title = "Change sort direction" 336 + , value = Command (UI.TracksMsg <| Tracks.SortBy model.sortBy) 337 + } 338 + ] 339 + ++ sortCommands 340 + ++ groupCommands 341 + 342 + 343 + 344 + -- ㊙️ 345 + 346 + 347 + action { result } = 348 + case Maybe.andThen (.value >> Alfred.command) result of 349 + Just msg -> 350 + [ msg ] 351 + 352 + Nothing -> 353 + [] 354 + 355 + 356 + toggle bool suffix = 357 + ifThenElse bool "Disable" "Enable" ++ " " ++ suffix
+16
src/Applications/UI/Commands/State.elm
··· 1 + module UI.Commands.State exposing (..) 2 + 3 + import UI.Alfred.State as Alfred 4 + import UI.Commands.Alfred 5 + import UI.Types exposing (Manager) 6 + 7 + 8 + 9 + -- 📣 10 + 11 + 12 + showPalette : Manager 13 + showPalette model = 14 + Alfred.assign 15 + (UI.Commands.Alfred.palette model) 16 + model
+3
src/Applications/UI/ContextMenu.elm
··· 42 42 [ "absolute" 43 43 , "bg-white" 44 44 , "leading-loose" 45 + , "opacity-95" 45 46 , "overflow-hidden" 46 47 , "-translate-x-1/2" 47 48 , "-translate-y-1/2" ··· 55 56 -- Dark mode 56 57 ------------ 57 58 , "dark:bg-darkest-hour" 59 + , "dark:border" 60 + , "dark:border-base00" 58 61 ] 59 62 (List.map 60 63 (\item ->
+59 -31
src/Applications/UI/Playlists/Alfred.elm
··· 1 1 module UI.Playlists.Alfred exposing (create, select) 2 2 3 3 import Alfred exposing (..) 4 + import Json.Decode exposing (string) 4 5 import List.Extra as List 6 + import Material.Icons as Icons 5 7 import Playlists exposing (..) 6 8 import Tracks exposing (IdentifiedTrack) 7 9 import UI.Types as UI ··· 18 20 playlists 19 21 |> List.map .name 20 22 |> List.sortBy String.toLower 23 + 24 + index = 25 + makeIndex playlistNames 21 26 in 22 - { action = createAction tracks 23 - , focus = 0 24 - , index = playlistNames 25 - , message = 26 - if List.length tracks == 1 then 27 - "Choose or create a playlist to add this track to." 27 + Alfred.create 28 + { action = createAction tracks 29 + , index = index 30 + , message = 31 + if List.length tracks == 1 then 32 + "Choose or create a playlist to add this track to." 28 33 29 - else 30 - "Choose or create a playlist to add these tracks to." 31 - , operation = QueryOrMutation 32 - , results = playlistNames 33 - , searchTerm = Nothing 34 - } 34 + else 35 + "Choose or create a playlist to add these tracks to." 36 + , operation = QueryOrMutation 37 + } 35 38 36 39 37 - createAction : List IdentifiedTrack -> { result : Maybe String, searchTerm : Maybe String } -> List UI.Msg 38 - createAction tracks maybe = 40 + createAction : List IdentifiedTrack -> Alfred.Action UI.Msg 41 + createAction tracks ctx = 39 42 let 40 43 playlistTracks = 41 44 Tracks.toPlaylistTracks tracks 42 45 in 43 - case maybe.result of 46 + case ctx.result of 44 47 Just result -> 45 48 -- Add to playlist 46 49 -- 47 - [ UI.AddTracksToPlaylist 48 - { playlistName = result 49 - , tracks = playlistTracks 50 - } 51 - ] 50 + case Alfred.stringValue result.value of 51 + Just playlistName -> 52 + [ UI.AddTracksToPlaylist 53 + { playlistName = playlistName 54 + , tracks = playlistTracks 55 + } 56 + ] 57 + 58 + Nothing -> 59 + [] 52 60 53 61 Nothing -> 54 62 -- Create playlist, 55 63 -- if given a search term. 56 64 -- 57 - case maybe.searchTerm of 65 + case ctx.searchTerm of 58 66 Just searchTerm -> 59 67 [ UI.AddTracksToPlaylist 60 68 { playlistName = searchTerm ··· 77 85 playlists 78 86 |> List.map .name 79 87 |> List.sortBy String.toLower 88 + 89 + index = 90 + makeIndex playlistNames 80 91 in 81 - { action = selectAction playlists 82 - , focus = 0 83 - , index = playlistNames 84 - , message = "Select a playlist to play tracks from." 85 - , operation = Query 86 - , results = playlistNames 87 - , searchTerm = Nothing 88 - } 92 + Alfred.create 93 + { action = selectAction playlists 94 + , index = index 95 + , message = "Select a playlist to play tracks from." 96 + , operation = Query 97 + } 89 98 90 99 91 - selectAction : List Playlist -> { result : Maybe String, searchTerm : Maybe String } -> List UI.Msg 100 + selectAction : List Playlist -> Alfred.Action UI.Msg 92 101 selectAction playlists { result } = 93 - case Maybe.andThen (\r -> List.find (.name >> (==) r) playlists) result of 102 + case Maybe.andThen (\r -> List.find (.name >> Just >> (==) (stringValue r.value)) playlists) result of 94 103 Just playlist -> 95 104 [ UI.SelectPlaylist playlist ] 96 105 97 106 Nothing -> 98 107 [] 108 + 109 + 110 + 111 + -- ㊙️ 112 + 113 + 114 + makeIndex playlistNames = 115 + playlistNames 116 + |> List.map 117 + (\name -> 118 + { icon = Just (Icons.queue_music 16) 119 + , title = name 120 + , value = Alfred.StringValue name 121 + } 122 + ) 123 + |> (\items -> 124 + { name = Nothing, items = items } 125 + ) 126 + |> List.singleton
+3
src/Applications/UI/Ports.elm
··· 22 22 port loadAlbumCovers : { list : Bool, coverView : Bool } -> Cmd msg 23 23 24 24 25 + port openUrlOnNewPage : String -> Cmd msg 26 + 27 + 25 28 port pause : () -> Cmd msg 26 29 27 30
+9 -1
src/Applications/UI/Routing/State.elm
··· 1 - module UI.Routing.State exposing (linkClicked, resetUrl, transition, urlChanged) 1 + module UI.Routing.State exposing (linkClicked, openUrlOnNewPage, resetUrl, transition, urlChanged) 2 2 3 3 import Browser exposing (UrlRequest) 4 4 import Browser.Navigation as Nav ··· 10 10 import Sources.Services.Google 11 11 import UI.Common.State as Common 12 12 import UI.Page as Page exposing (Page) 13 + import UI.Ports as Ports 13 14 import UI.Sources.Form 14 15 import UI.Sources.Page 15 16 import UI.Sources.State as Sources ··· 42 43 43 44 Browser.External href -> 44 45 return model (Nav.load href) 46 + 47 + 48 + openUrlOnNewPage : String -> Manager 49 + openUrlOnNewPage url model = 50 + url 51 + |> Ports.openUrlOnNewPage 52 + |> return model 45 53 46 54 47 55 urlChanged : Url -> Manager
+82
src/Applications/UI/Tracks/State.elm
··· 103 103 Add a -> 104 104 add a 105 105 106 + AddFavourites a -> 107 + addFavourites a 108 + 106 109 Reload a -> 107 110 reload a 108 111 ··· 111 114 112 115 RemoveBySourceId a -> 113 116 removeBySourceId a 117 + 118 + RemoveFavourites a -> 119 + removeFavourites a 114 120 115 121 SortBy a -> 116 122 sortBy a ··· 189 195 |> Collection.add 190 196 ) 191 197 model 198 + 199 + 200 + addFavourites : List IdentifiedTrack -> Manager 201 + addFavourites = 202 + manageFavourites AddToFavourites 192 203 193 204 194 205 changeScene : Scene -> Manager ··· 379 390 |> Return.singleton 380 391 381 392 393 + manageFavourites : FavouritesManagementAction -> List IdentifiedTrack -> Manager 394 + manageFavourites action tracks model = 395 + let 396 + newFavourites = 397 + (case action of 398 + AddToFavourites -> 399 + Favourites.completeFavouritesList 400 + 401 + RemoveFromFavourites -> 402 + Favourites.removeFromFavouritesList 403 + ) 404 + tracks 405 + model.favourites 406 + 407 + effect collection = 408 + collection 409 + |> Collection.map 410 + (case action of 411 + AddToFavourites -> 412 + Favourites.completeTracksList tracks 413 + 414 + RemoveFromFavourites -> 415 + Favourites.removeFromTracksList tracks 416 + ) 417 + |> (if model.favouritesOnly then 418 + Collection.harvest 419 + 420 + else 421 + identity 422 + ) 423 + 424 + selectedCover = 425 + Maybe.map 426 + (\cover -> 427 + cover.tracks 428 + |> (case action of 429 + AddToFavourites -> 430 + Favourites.completeTracksList tracks 431 + 432 + RemoveFromFavourites -> 433 + Favourites.removeFromTracksList tracks 434 + ) 435 + |> (\a -> { cover | tracks = a }) 436 + ) 437 + model.selectedCover 438 + in 439 + { model | favourites = newFavourites, selectedCover = selectedCover } 440 + |> reviseCollection effect 441 + |> andThen User.saveFavourites 442 + |> (if model.scene == Covers then 443 + andThen generateCovers >> andThen harvestCovers 444 + 445 + else 446 + identity 447 + ) 448 + 449 + 382 450 markAsSelected : Int -> { shiftKey : Bool } -> Manager 383 451 markAsSelected indexInList { shiftKey } model = 384 452 let ··· 456 524 |> return { model | tracks = newCollection } 457 525 |> andThen (reviseCollection Collection.identify) 458 526 |> andThen (removeFromCache removed) 527 + 528 + 529 + removeFavourites : List IdentifiedTrack -> Manager 530 + removeFavourites = 531 + manageFavourites RemoveFromFavourites 459 532 460 533 461 534 removeFromCache : List Track -> Manager ··· 976 1049 Nothing -> 977 1050 andThen (Common.toggleLoadingScreen Off) 978 1051 ) 1052 + 1053 + 1054 + 1055 + -- ㊙️ 1056 + 1057 + 1058 + type FavouritesManagementAction 1059 + = AddToFavourites 1060 + | RemoveFromFavourites
+2
src/Applications/UI/Tracks/Types.elm
··· 37 37 -- Collection 38 38 ----------------------------------------- 39 39 | Add Json.Value 40 + | AddFavourites (List IdentifiedTrack) 40 41 | Reload Json.Value 41 42 | RemoveByPaths Json.Value 42 43 | RemoveBySourceId String 44 + | RemoveFavourites (List IdentifiedTrack) 43 45 | SortBy SortBy 44 46 | ToggleFavourite Int 45 47 -----------------------------------------
+1
src/Applications/UI/Types.elm
··· 261 261 ----------------------------------------- 262 262 | ChangeUrlUsingPage Page 263 263 | LinkClicked Browser.UrlRequest 264 + | OpenUrlOnNewPage String 264 265 | PageChanged Page 265 266 | UrlChanged Url 266 267 -----------------------------------------
+3 -3
src/Applications/UI/View.elm
··· 282 282 brick 283 283 [] 284 284 [ "inset-0" 285 - , "bg-black" 286 - , "duration-1000" 285 + , "bg-darkest-hour" 286 + , "duration-500" 287 287 , "ease-in-out" 288 288 , "fixed" 289 289 , "transition-opacity" ··· 291 291 292 292 -- 293 293 , ifThenElse isShown "pointer-events-auto" "pointer-events-none" 294 - , ifThenElse isShown "opacity-40" "opacity-0" 294 + , ifThenElse isShown "opacity-50" "opacity-0" 295 295 ] 296 296 [] 297 297
+5
src/Javascript/index.js
··· 81 81 wire.covers() 82 82 wire.webnative() 83 83 84 + // Other ports 85 + app.ports.openUrlOnNewPage.subscribe(url => { 86 + window.open(url, "_blank") 87 + }) 88 + 84 89 // Check for service worker updates every hour 85 90 setInterval(() => reg.update(), 1 * 1000 * 60 * 60) 86 91 }
+94 -4
src/Library/Alfred.elm
··· 1 1 module Alfred exposing (..) 2 2 3 + import List.Extra as List 4 + import Material.Icons.Types exposing (Coloring) 5 + import Svg exposing (Svg) 6 + 7 + 8 + 3 9 -- 🌳 4 10 5 11 6 - type alias Alfred action = 7 - { action : { result : Maybe String, searchTerm : Maybe String } -> List action 12 + type alias Alfred msg = 13 + { action : Action msg 8 14 , focus : Int 9 - , index : List String 15 + , index : List (Group msg) 16 + , indexFlattened : List (Item msg) 10 17 , message : String 11 18 , operation : Operation 12 - , results : List String 19 + , results : List (Group msg) 13 20 , searchTerm : Maybe String 14 21 } 15 22 16 23 24 + type alias Action msg = 25 + { result : Maybe (Item msg), searchTerm : Maybe String } -> List msg 26 + 27 + 28 + type alias Group msg = 29 + { name : Maybe String, items : List (Item msg) } 30 + 31 + 32 + type alias Item msg = 33 + { icon : Maybe (Coloring -> Svg msg) 34 + , title : String 35 + , value : ItemValue msg 36 + } 37 + 38 + 39 + type ItemValue msg 40 + = Command msg 41 + | StringValue String 42 + 43 + 17 44 type Operation 18 45 = Query 19 46 | QueryOrMutation 20 47 | Mutation 48 + 49 + 50 + 51 + -- 🛳 52 + 53 + 54 + create : 55 + { action : Action msg 56 + , index : List (Group msg) 57 + , message : String 58 + , operation : Operation 59 + } 60 + -> Alfred msg 61 + create { action, index, message, operation } = 62 + { action = action 63 + , focus = 0 64 + , index = index 65 + , indexFlattened = List.concatMap .items index 66 + , message = message 67 + , operation = operation 68 + , results = index 69 + , searchTerm = Nothing 70 + } 71 + 72 + 73 + 74 + -- 🛠 75 + 76 + 77 + command : ItemValue msg -> Maybe msg 78 + command val = 79 + case val of 80 + Command cmd -> 81 + Just cmd 82 + 83 + StringValue _ -> 84 + Nothing 85 + 86 + 87 + stringValue : ItemValue msg -> Maybe String 88 + stringValue val = 89 + case val of 90 + Command _ -> 91 + Nothing 92 + 93 + StringValue string -> 94 + Just string 95 + 96 + 97 + 98 + -- 🛠 99 + 100 + 101 + getAt : Int -> Alfred msg -> Maybe (Item msg) 102 + getAt index alfred = 103 + alfred.results 104 + |> List.concatMap .items 105 + |> List.getAt index 106 + 107 + 108 + length : Alfred msg -> Int 109 + length { indexFlattened } = 110 + List.length indexFlattened
+104 -1
src/Library/Tracks/Favourites.elm
··· 1 - module Tracks.Favourites exposing (match, simplified, toggleInFavouritesList, toggleInTracksList) 1 + module Tracks.Favourites exposing (completeFavouritesList, completeTracksList, match, removeFromFavouritesList, removeFromTracksList, simplified, toggleInFavouritesList, toggleInTracksList) 2 2 3 3 import List.Extra as List 4 4 import Tracks exposing (Favourite, IdentifiedTrack, Track) ··· 8 8 -- 🔱 9 9 10 10 11 + completeFavouritesList : List IdentifiedTrack -> List Favourite -> List Favourite 12 + completeFavouritesList tracks favourites = 13 + List.append 14 + favourites 15 + (List.filterMap 16 + (\( i, t ) -> 17 + if not i.isFavourite then 18 + Just 19 + { artist = t.tags.artist 20 + , title = t.tags.title 21 + } 22 + 23 + else 24 + Nothing 25 + ) 26 + tracks 27 + ) 28 + 29 + 30 + completeTracksList : List IdentifiedTrack -> List IdentifiedTrack -> List IdentifiedTrack 31 + completeTracksList tracksToMakeFavourite tracks = 32 + let 33 + favs = 34 + List.filterMap 35 + (\( i, t ) -> 36 + if not i.isFavourite then 37 + Just ( lowercaseArtist t, lowercaseTitle t ) 38 + 39 + else 40 + Nothing 41 + ) 42 + tracksToMakeFavourite 43 + in 44 + List.map 45 + (\( i, t ) -> 46 + let 47 + ( la, lt ) = 48 + ( lowercaseArtist t, lowercaseTitle t ) 49 + in 50 + List.foldr 51 + (\( lartist, ltitle ) ( ai, at ) -> 52 + if la == lartist && lt == ltitle && not ai.isFavourite then 53 + ( { ai | isFavourite = True }, at ) 54 + 55 + else 56 + ( ai, at ) 57 + ) 58 + ( i, t ) 59 + favs 60 + ) 61 + tracks 62 + 63 + 11 64 match : Favourite -> Favourite -> Bool 12 65 match a b = 13 66 let ··· 22 75 ) 23 76 in 24 77 aa == ba && at == bt 78 + 79 + 80 + removeFromFavouritesList : List IdentifiedTrack -> List Favourite -> List Favourite 81 + removeFromFavouritesList tracks favourites = 82 + List.foldr 83 + (\( i, t ) acc -> 84 + if i.isFavourite then 85 + List.filterNot 86 + (match { artist = t.tags.artist, title = t.tags.title }) 87 + acc 88 + 89 + else 90 + acc 91 + ) 92 + favourites 93 + tracks 94 + 95 + 96 + removeFromTracksList : List IdentifiedTrack -> List IdentifiedTrack -> List IdentifiedTrack 97 + removeFromTracksList tracksToRemoveFromFavs tracks = 98 + let 99 + unfavs = 100 + List.filterMap 101 + (\( i, t ) -> 102 + if i.isFavourite then 103 + Just ( lowercaseArtist t, lowercaseTitle t ) 104 + 105 + else 106 + Nothing 107 + ) 108 + tracksToRemoveFromFavs 109 + in 110 + List.map 111 + (\( i, t ) -> 112 + let 113 + ( la, lt ) = 114 + ( lowercaseArtist t, lowercaseTitle t ) 115 + in 116 + List.foldr 117 + (\( lartist, ltitle ) ( ai, at ) -> 118 + if la == lartist && lt == ltitle && ai.isFavourite then 119 + ( { ai | isFavourite = False }, at ) 120 + 121 + else 122 + ( ai, at ) 123 + ) 124 + ( i, t ) 125 + unfavs 126 + ) 127 + tracks 25 128 26 129 27 130 simplified : Favourite -> String
+18 -16
src/Static/About/Index.md
··· 96 96 97 97 The app should be usable with only the keyboard, there are various keyboard shortcuts: 98 98 99 - ``` 100 - L - Select playlist using autocompletion 101 - N - Scroll to currently-playing track 102 - P - Play / Pause 103 - R - Toggle Repeat 104 - S - Toggle Shuffle 99 + ```js 100 + CTRL + K or CMD + K // Show command palette 105 101 106 - { / } - Previous / Next 107 - < / > - Seek forwards / Seek backwards 102 + CTRL + L // Select playlist using autocompletion 103 + CTRL + N // Scroll to currently-playing track 104 + CTRL + P // Play / Pause 105 + CTRL + R // Toggle Repeat 106 + CTRL + S // Toggle Shuffle 108 107 109 - Alternatively you can use the media-control keys, 108 + CTRL + [ or ] // Previous / Next 109 + CTRL + { or } // Seek forwards / Seek backwards 110 + 111 + Alternatively you can use the media-control keys, 110 112 if your browser supports it. 111 113 112 - ESC - Close overlay, close context-menu, deselect album cover, etc. 114 + ESC // Close overlay, close context-menu, deselect album cover, etc. 113 115 114 - 1 - Tracks 115 - 2 - Playlists 116 - 3 - Queue 117 - 4 - EQ 116 + CTRL + 1 // Tracks 117 + CTRL + 2 // Playlists 118 + CTRL + 3 // Queue 119 + CTRL + 4 // EQ 118 120 119 - 8 - Sources 120 - 9 - Settings 121 + CTRL + 8 // Sources 122 + CTRL + 9 // Settings 121 123 ``` 122 124 123 125
+3
system/Css/Tailwind.js
··· 205 205 "05": ".05", 206 206 "10": ".1", 207 207 "20": ".2", 208 + "25": ".25", 208 209 "30": ".3", 209 210 "40": ".4", 210 211 "50": ".5", 211 212 "60": ".6", 212 213 "70": ".7", 214 + "75": ".75", 213 215 "80": ".8", 214 216 "90": ".9", 217 + "95": ".95", 215 218 "100": "1" 216 219 }, 217 220