diff --git a/src/Applications/UI.elm b/src/Applications/UI.elm index 738f6ed0..64d7f22f 100644 --- a/src/Applications/UI.elm +++ b/src/Applications/UI.elm @@ -432,6 +432,9 @@ update msg = LinkClicked a -> Routing.linkClicked a + OpenUrlOnNewPage a -> + Routing.openUrlOnNewPage a + PageChanged a -> Routing.transition a diff --git a/src/Applications/UI/Adjunct.elm b/src/Applications/UI/Adjunct.elm index 94505f51..75ce16bd 100644 --- a/src/Applications/UI/Adjunct.elm +++ b/src/Applications/UI/Adjunct.elm @@ -6,6 +6,7 @@ import Return import UI.Alfred.State as Alfred import UI.Audio.State as Audio import UI.Authentication.Common as Authentication +import UI.Commands.State as Commands import UI.Common.State as Common import UI.Interface.State exposing (hideOverlay) import UI.Page as Page @@ -60,51 +61,60 @@ keyboardInput msg model = else case m.pressedKeys of - [ Keyboard.Character "{", Keyboard.Shift ] -> + [ Keyboard.Character "[", Keyboard.Control ] -> Queue.rewind m - [ Keyboard.Character "}", Keyboard.Shift ] -> + [ Keyboard.Character "]", Keyboard.Control ] -> Queue.shift m - [ Keyboard.Character "<", Keyboard.Shift ] -> + [ Keyboard.Character "{", Keyboard.Shift, Keyboard.Control ] -> Audio.seek ((m.audioPosition - 10) / m.audioDuration) m - [ Keyboard.Character ">", Keyboard.Shift ] -> + [ Keyboard.Character "}", Keyboard.Shift, Keyboard.Control ] -> Audio.seek ((m.audioPosition + 10) / m.audioDuration) m + -- Meta key -- - [ Keyboard.Character "L" ] -> + [ Keyboard.Character "K", Keyboard.Meta ] -> + Commands.showPalette m + + -- Ctrl key + -- + [ Keyboard.Character "K", Keyboard.Control ] -> + Commands.showPalette m + + [ Keyboard.Character "L", Keyboard.Control ] -> Playlists.assistWithSelectingPlaylist m - [ Keyboard.Character "N" ] -> + [ Keyboard.Character "N", Keyboard.Control ] -> Tracks.scrollToNowPlaying m - [ Keyboard.Character "P" ] -> + [ Keyboard.Character "P", Keyboard.Control ] -> Audio.playPause m - [ Keyboard.Character "R" ] -> + [ Keyboard.Character "R", Keyboard.Control ] -> Queue.toggleRepeat m - [ Keyboard.Character "S" ] -> + [ Keyboard.Character "S", Keyboard.Control ] -> Queue.toggleShuffle m -- - [ Keyboard.Character "1" ] -> + [ Keyboard.Character "1", Keyboard.Control ] -> Common.changeUrlUsingPage Page.Index m - [ Keyboard.Character "2" ] -> + [ Keyboard.Character "2", Keyboard.Control ] -> Common.changeUrlUsingPage (Page.Playlists Playlists.Index) m - [ Keyboard.Character "3" ] -> + [ Keyboard.Character "3", Keyboard.Control ] -> Common.changeUrlUsingPage (Page.Queue Queue.Index) m - [ Keyboard.Character "4" ] -> + [ Keyboard.Character "4", Keyboard.Control ] -> Common.changeUrlUsingPage Page.Equalizer m - [ Keyboard.Character "8" ] -> + [ Keyboard.Character "8", Keyboard.Control ] -> Common.changeUrlUsingPage (Page.Sources Sources.Index) m - [ Keyboard.Character "9" ] -> + [ Keyboard.Character "9", Keyboard.Control ] -> Common.changeUrlUsingPage (Page.Settings Settings.Index) m -- diff --git a/src/Applications/UI/Alfred/State.elm b/src/Applications/UI/Alfred/State.elm index 429ed36d..e7bd3bf7 100644 --- a/src/Applications/UI/Alfred/State.elm +++ b/src/Applications/UI/Alfred/State.elm @@ -20,7 +20,10 @@ assign instance model = |> Process.sleep |> Task.andThen (\_ -> Dom.focus "diffuse__alfred") |> Task.attempt (\_ -> UI.Bypass) - |> return { model | alfred = Just instance } + -- The "K" key seems to stick when using CMD + K, + -- aka. Meta key + K, to show the command palette. + -- https://github.com/ohanhi/keyboard/issues/14 + |> return { model | alfred = Just instance, pressedKeys = [] } gotInput : String -> Manager @@ -34,7 +37,7 @@ runAction : Int -> Manager runAction index model = case model.alfred of Just instance -> - { result = List.getAt index instance.results + { result = Alfred.getAt index instance , searchTerm = instance.searchTerm } |> instance.action @@ -78,8 +81,12 @@ selectNext : Manager selectNext model = case model.alfred of Just instance -> + let + total = + Alfred.length instance + in instance - |> (\i -> { i | focus = min (i.focus + 1) (List.length i.results - 1) }) + |> (\i -> { i | focus = min (i.focus + 1) (total - 1) }) |> (\i -> { model | alfred = Just i }) |> scrollToFocus @@ -118,9 +125,16 @@ determineResults searchTerm alfred = , searchTerm = Just searchTerm , results = - alfred.index - |> List.filter (String.toLower >> String.contains lowerSearchTerm) - |> List.sort + List.map + (\group -> + group.items + |> List.filter + (.title >> String.toLower >> String.contains lowerSearchTerm) + |> (\items -> + { group | items = items } + ) + ) + alfred.index } else diff --git a/src/Applications/UI/Alfred/View.elm b/src/Applications/UI/Alfred/View.elm index 6ebf3529..5e9d68cd 100644 --- a/src/Applications/UI/Alfred/View.elm +++ b/src/Applications/UI/Alfred/View.elm @@ -43,6 +43,7 @@ view maybeInstance extractedBackdropColor = [ "italic" , "leading-normal" , "mt-12" + , "opacity-75" , "text-center" , "text-white" @@ -66,7 +67,7 @@ view maybeInstance extractedBackdropColor = ) ] [ "text-sm" - , "max-w-lg" + , "max-w-xl" , "mt-8" , "w-full" ] @@ -88,21 +89,23 @@ view maybeInstance extractedBackdropColor = Mutation -> placeholder "Type to create" ] - [ "border-none" + [ "border" , "bg-white" , "block" , "leading-normal" - , "rounded" + , "opacity-95" , "outline-none" , "p-4" + , "rounded-t" , "shadow-md" - , "text-2xl" + , "text-xl" , "tracking-tad-closer" , "w-full" -- Dark mode ------------ - , "dark:bg-base00" + , "dark:bg-darkest-hour" + , "dark:border-base00" ] [] ] @@ -113,13 +116,15 @@ view maybeInstance extractedBackdropColor = , brick [ id "alfred__results" ] [ "bg-white" - , "rounded" - , "leading-none" - , "max-w-lg" + , "border" + , "border-t-0" + , "leading-tight" + , "max-w-xl" , "mb-32" - , "mt-8" + , "opacity-95" , "overflow-x-hidden" , "overflow-y-auto" + , "rounded-b" , "shadow-md" , "smooth-scrolling" , "text-nearly-sm" @@ -127,71 +132,133 @@ view maybeInstance extractedBackdropColor = -- Dark mode ------------ - , "dark:bg-base00" + , "dark:bg-darkest-hour" + , "dark:border-base00" ] - (List.indexedMap - (\idx result -> - brick - [ onTapPreventDefault (UI.SelectAlfredItem idx) - - -- - , if idx == instance.focus then - id "alfred__results__focus" - - else - id ("alfred__results__" ++ String.fromInt idx) - - -- - , if idx == instance.focus then - style "background-color" bgColor - - else - style "" "" - ] - (List.concat - [ [ "p-4" - , "relative" - , "truncate" - ] - - -- - , if idx == instance.focus then - [ "text-white", "dark:text-base07" ] - - else - [ "text-inherit" ] - - -- - , if modBy 2 idx == 0 then - [] - - else - [ "bg-gray-100", "dark:bg-base01-15" ] - ] - ) - [ text result - - -- - , if idx == instance.focus then - chunk - [ "absolute" - , "leading-0" - , "-translate-y-1/2" - , "mr-3" - , "right-0" - , "top-1/2" - , "transform" - ] - [ Icons.keyboard_return 13 Inherit - ] - - else - nothing - ] - ) - instance.results + (instance.results + |> List.foldl + (\group ( acc, indexBase ) -> + case List.length group.items of + 0 -> + ( acc, indexBase ) + + x -> + ( groupView bgColor instance group indexBase :: acc + , indexBase + x + ) + ) + ( [], 0 ) + |> Tuple.first + |> List.reverse ) ] Nothing -> nothing + + +groupView bgColor instance group indexBase = + raw + [ case group.name of + Just name -> + chunk + [ "all-small-caps" + , "antialiased" + , "font-semibold" + , "leading-tight" + , "mb-2" + , "mx-2" + , "mt-5" + , "opacity-60" + , "px-3" + , "text-sm" + , "tracking-wider" + ] + [ Html.text name ] + + Nothing -> + Html.text "" + , raw + (List.indexedMap + (\i -> itemView bgColor instance <| indexBase + i) + group.items + ) + ] + + +itemView bgColor instance idx item = + brick + [ onTapPreventDefault (UI.SelectAlfredItem idx) + + -- + , if idx == instance.focus then + id "alfred__results__focus" + + else + id ("alfred__results__" ++ String.fromInt idx) + + -- + , if idx == instance.focus then + style "background-color" bgColor + + else + style "" "" + ] + (List.concat + [ [ "flex" + , "items-center" + , "m-2" + , "p-3" + , "relative" + , "rounded" + ] + + -- + , if idx == instance.focus then + [ "text-white" + , "dark:opacity-80" + , "dark:text-base07" + ] + + else + [ "text-inherit" ] + + -- + -- , if modBy 2 idx == 0 then + -- [] + -- else + -- [ "bg-gray-100", "dark:bg-base01-15" ] + ] + ) + [ case item.icon of + Just icon -> + slab + Html.span + [] + [ "inline-block" + , "mr-2" + , "w-5" + ] + [ icon Inherit + ] + + Nothing -> + text "" + + -- + , slab + Html.span + [] + [ "flex-1", "inline-block", "pt-px" ] + [ text item.title ] + + -- + , if idx == instance.focus then + chunk + [ "leading-0", "ml-2" ] + [ Icons.keyboard_return 13 Inherit + ] + + else + nothing + ] diff --git a/src/Applications/UI/Authentication/View.elm b/src/Applications/UI/Authentication/View.elm index 8ca3127b..9187c555 100644 --- a/src/Applications/UI/Authentication/View.elm +++ b/src/Applications/UI/Authentication/View.elm @@ -222,7 +222,6 @@ welcomeScreen = ] [ "align-middle" , "inline-block" - , "pt-px" , "text-nearly-sm" ] [ text "SIGN IN" ] diff --git a/src/Applications/UI/Commands/Alfred.elm b/src/Applications/UI/Commands/Alfred.elm new file mode 100644 index 00000000..99b58a33 --- /dev/null +++ b/src/Applications/UI/Commands/Alfred.elm @@ -0,0 +1,357 @@ +module UI.Commands.Alfred exposing (commands, palette) + +import Alfred exposing (..) +import Conditional exposing (ifThenElse) +import List.Extra as List +import Material.Icons as Icons +import Tracks exposing (Grouping(..), SortBy(..)) +import UI.Page as Page +import UI.Queue.Types as Queue +import UI.Sources.Page as Sources +import UI.Sources.Types as Sources +import UI.Tracks.Types as Tracks +import UI.Types as UI + + +palette : UI.Model -> Alfred UI.Msg +palette model = + Alfred.create + { action = action + , index = commands model + , message = "Run a command." + , operation = Query + } + + + +-- ⛰ + + +commands : UI.Model -> List (Alfred.Group UI.Msg) +commands model = + [ { name = Just "Currently playing", items = nowPlayingCommands model } + , { name = Just "Track selection", items = selectionCommands model } + , { name = Just "View", items = viewCommands model } + , { name = Just "Playback", items = playbackCommands model } + , { name = Just "Sources", items = sourcesCommands model } + , { name = Just "Data", items = dataCommands model } + , { name = Just "Misc", items = miscCommands model } + ] + + + +-- + + +dataCommands model = + [ { icon = Just (Icons.offline_bolt 16) + , title = "Clear tracks cache" + , value = Command (UI.TracksMsg Tracks.ClearCache) + } + , { icon = Just (Icons.save 16) + , title = "Export data" + , value = Command UI.Export + } + , { icon = Just (Icons.save 16) + , title = "Import data (⚠️ will override current data)" + , value = Command UI.RequestImport + } + , { icon = Just (Icons.save 16) + , title = "Migrate user data to different storage" + , value = Command UI.MigrateHypaethralUserData + } + ] + + +miscCommands model = + [ { icon = Just (Icons.help 16) + , title = "Open help" + , value = Command (UI.OpenUrlOnNewPage "./about/#UI") + } + ] + + +nowPlayingCommands : UI.Model -> List (Item UI.Msg) +nowPlayingCommands model = + case model.nowPlaying of + Just queueItem -> + let + ( queueItemIdentifiers, _ ) = + queueItem.identifiedTrack + + identifiedTrack = + model.tracks.harvested + |> List.getAt queueItemIdentifiers.indexInList + |> Maybe.withDefault queueItem.identifiedTrack + + ( identifiers, track ) = + identifiedTrack + in + [ { icon = Just (Icons.search 16) + , title = "Show current track in list" + , value = Command (UI.TracksMsg Tracks.ScrollToNowPlaying) + } + + -- + , { icon = Just (Icons.favorite 14) + , title = ifThenElse identifiers.isFavourite "Remove favourite" "Mark as favourite" + , value = Command (UI.TracksMsg <| Tracks.ToggleFavourite identifiers.indexInList) + } + + -- + , { icon = Just (Icons.queue_music 16) + , title = "Add current track to playlist" + , value = Command (UI.AssistWithAddingTracksToPlaylist <| [ identifiedTrack ]) + } + + -- + , { icon = Just (Icons.offline_bolt 16) + , title = "Add current track to cache" + , value = + [ track ] + |> Tracks.StoreInCache + |> UI.TracksMsg + |> Command + } + ] + + Nothing -> + [] + + +playbackCommands model = + [ if model.audioIsPlaying then + { icon = Just (Icons.pause 16) + , title = "Pause" + , value = Command UI.TogglePlay + } + + else + { icon = Just (Icons.play_arrow 16) + , title = "Play" + , value = Command UI.TogglePlay + } + + -- + , { icon = Just (Icons.fast_rewind 18) + , title = "Previous track" + , value = Command (UI.QueueMsg Queue.Rewind) + } + , { icon = Just (Icons.fast_forward 18) + , title = "Next track" + , value = Command (UI.QueueMsg Queue.Shift) + } + , { icon = Just (Icons.repeat 16) + , title = toggle model.repeat "repeat" + , value = Command (UI.QueueMsg Queue.ToggleRepeat) + } + , { icon = Just (Icons.shuffle 16) + , title = toggle model.shuffle "shuffle" + , value = Command (UI.QueueMsg Queue.ToggleShuffle) + } + ] + + +selectionCommands model = + let + ( selection, _, amountOfFavs ) = + List.foldr + (\( i, t ) ( acc, selected, favouriteCounter ) -> + case List.findIndex ((==) i.indexInList) selected of + Just s -> + ( ( i, t ) :: acc + , List.removeAt s selected + , ifThenElse i.isFavourite (favouriteCounter + 1) favouriteCounter + ) + + Nothing -> + ( acc, selected, favouriteCounter ) + ) + ( [] + , model.selectedTrackIndexes + , 0 + ) + model.tracks.harvested + in + case selection of + [] -> + [] + + tracks -> + List.concat + [ [ { icon = Just (Icons.queue_music 16) + , title = "Add current selection to playlist" + , value = Command (UI.AssistWithAddingTracksToPlaylist tracks) + } + ] + + -- + , if amountOfFavs > 0 then + [ { icon = Just (Icons.favorite 14) + , title = "Remove current selection from favourites" + , value = Command (UI.TracksMsg <| Tracks.RemoveFavourites tracks) + } + ] + + else + [] + + -- + , if amountOfFavs < List.length selection then + [ { icon = Just (Icons.favorite 14) + , title = "Add current selection to favourites" + , value = Command (UI.TracksMsg <| Tracks.AddFavourites tracks) + } + ] + + else + [] + ] + + +sourcesCommands model = + [ { icon = Just (Icons.sync 16) + , title = "Process sources" + , value = Command (UI.SourcesMsg Sources.Process) + } + + -- + , { icon = Just (Icons.add 16) + , title = "Add new source" + , value = Command (UI.ChangeUrlUsingPage <| Page.Sources Sources.New) + } + ] + + +viewCommands model = + let + sortCommands = + (case Maybe.map .autoGenerated model.selectedPlaylist of + Just False -> + [] + + _ -> + case model.scene of + Tracks.Covers -> + [ Album, Artist ] + + Tracks.List -> + [ Album, Artist, Title ] + ) + |> List.remove + model.sortBy + |> List.map + (\sortBy -> + { icon = + Just (Icons.sort 16) + , title = + case sortBy of + Artist -> + "Sort tracks by artist" + + Album -> + "Sort tracks by album" + + PlaylistIndex -> + "Sort tracks by playlist index" + + Title -> + "Sort tracks by title" + , value = + Command (UI.TracksMsg <| Tracks.SortBy sortBy) + } + ) + + groupCommands = + [ AddedOn, Directory, FirstAlphaCharacter, TrackYear ] + |> (case model.grouping of + Just group -> + List.remove group + + Nothing -> + identity + ) + |> List.map + (\group -> + { icon = + Just (Icons.library_music 16) + , title = + case group of + AddedOn -> + "Group by processing date" + + Directory -> + "Group by directory" + + FirstAlphaCharacter -> + "Group by first letter" + + TrackYear -> + "Group by track year" + , value = + Command (UI.TracksMsg <| Tracks.GroupBy group) + } + ) + |> (\list -> + case model.grouping of + Just _ -> + { icon = Just (Icons.library_music 16) + , title = "Disable grouping" + , value = Command (UI.TracksMsg Tracks.DisableGrouping) + } + :: list + + Nothing -> + list + ) + in + [ { icon = Just (Icons.favorite 14) + , title = toggle model.favouritesOnly "favourites-only mode" + , value = Command (UI.TracksMsg Tracks.ToggleFavouritesOnly) + } + + -- + , case model.scene of + Tracks.Covers -> + { icon = Just (Icons.notes 16) + , title = "Switch to list view" + , value = Command (UI.TracksMsg <| Tracks.ChangeScene Tracks.List) + } + + Tracks.List -> + { icon = Just (Icons.burst_mode 18) + , title = "Switch to cover view" + , value = Command (UI.TracksMsg <| Tracks.ChangeScene Tracks.Covers) + } + + -- + , { icon = Just (Icons.filter_list 16) + , title = ifThenElse model.cachedTracksOnly "Disable cached-tracks-only mode" "Only show cached tracks" + , value = Command (UI.TracksMsg Tracks.ToggleCachedOnly) + } + + -- + , { icon = Just (Icons.sort 16) + , title = "Change sort direction" + , value = Command (UI.TracksMsg <| Tracks.SortBy model.sortBy) + } + ] + ++ sortCommands + ++ groupCommands + + + +-- ㊙️ + + +action { result } = + case Maybe.andThen (.value >> Alfred.command) result of + Just msg -> + [ msg ] + + Nothing -> + [] + + +toggle bool suffix = + ifThenElse bool "Disable" "Enable" ++ " " ++ suffix diff --git a/src/Applications/UI/Commands/State.elm b/src/Applications/UI/Commands/State.elm new file mode 100644 index 00000000..3e623781 --- /dev/null +++ b/src/Applications/UI/Commands/State.elm @@ -0,0 +1,16 @@ +module UI.Commands.State exposing (..) + +import UI.Alfred.State as Alfred +import UI.Commands.Alfred +import UI.Types exposing (Manager) + + + +-- 📣 + + +showPalette : Manager +showPalette model = + Alfred.assign + (UI.Commands.Alfred.palette model) + model diff --git a/src/Applications/UI/ContextMenu.elm b/src/Applications/UI/ContextMenu.elm index bd6a16f7..b4cea294 100644 --- a/src/Applications/UI/ContextMenu.elm +++ b/src/Applications/UI/ContextMenu.elm @@ -42,6 +42,7 @@ view viewportWidth m = [ "absolute" , "bg-white" , "leading-loose" + , "opacity-95" , "overflow-hidden" , "-translate-x-1/2" , "-translate-y-1/2" @@ -55,6 +56,8 @@ view viewportWidth m = -- Dark mode ------------ , "dark:bg-darkest-hour" + , "dark:border" + , "dark:border-base00" ] (List.map (\item -> diff --git a/src/Applications/UI/Playlists/Alfred.elm b/src/Applications/UI/Playlists/Alfred.elm index 1ff222f3..6bfb242b 100644 --- a/src/Applications/UI/Playlists/Alfred.elm +++ b/src/Applications/UI/Playlists/Alfred.elm @@ -1,7 +1,9 @@ module UI.Playlists.Alfred exposing (create, select) import Alfred exposing (..) +import Json.Decode exposing (string) import List.Extra as List +import Material.Icons as Icons import Playlists exposing (..) import Tracks exposing (IdentifiedTrack) import UI.Types as UI @@ -18,43 +20,49 @@ create tracks playlists = playlists |> List.map .name |> List.sortBy String.toLower + + index = + makeIndex playlistNames in - { action = createAction tracks - , focus = 0 - , index = playlistNames - , message = - if List.length tracks == 1 then - "Choose or create a playlist to add this track to." - - else - "Choose or create a playlist to add these tracks to." - , operation = QueryOrMutation - , results = playlistNames - , searchTerm = Nothing - } - - -createAction : List IdentifiedTrack -> { result : Maybe String, searchTerm : Maybe String } -> List UI.Msg -createAction tracks maybe = + Alfred.create + { action = createAction tracks + , index = index + , message = + if List.length tracks == 1 then + "Choose or create a playlist to add this track to." + + else + "Choose or create a playlist to add these tracks to." + , operation = QueryOrMutation + } + + +createAction : List IdentifiedTrack -> Alfred.Action UI.Msg +createAction tracks ctx = let playlistTracks = Tracks.toPlaylistTracks tracks in - case maybe.result of + case ctx.result of Just result -> -- Add to playlist -- - [ UI.AddTracksToPlaylist - { playlistName = result - , tracks = playlistTracks - } - ] + case Alfred.stringValue result.value of + Just playlistName -> + [ UI.AddTracksToPlaylist + { playlistName = playlistName + , tracks = playlistTracks + } + ] + + Nothing -> + [] Nothing -> -- Create playlist, -- if given a search term. -- - case maybe.searchTerm of + case ctx.searchTerm of Just searchTerm -> [ UI.AddTracksToPlaylist { playlistName = searchTerm @@ -77,22 +85,42 @@ select playlists = playlists |> List.map .name |> List.sortBy String.toLower + + index = + makeIndex playlistNames in - { action = selectAction playlists - , focus = 0 - , index = playlistNames - , message = "Select a playlist to play tracks from." - , operation = Query - , results = playlistNames - , searchTerm = Nothing - } + Alfred.create + { action = selectAction playlists + , index = index + , message = "Select a playlist to play tracks from." + , operation = Query + } -selectAction : List Playlist -> { result : Maybe String, searchTerm : Maybe String } -> List UI.Msg +selectAction : List Playlist -> Alfred.Action UI.Msg selectAction playlists { result } = - case Maybe.andThen (\r -> List.find (.name >> (==) r) playlists) result of + case Maybe.andThen (\r -> List.find (.name >> Just >> (==) (stringValue r.value)) playlists) result of Just playlist -> [ UI.SelectPlaylist playlist ] Nothing -> [] + + + +-- ㊙️ + + +makeIndex playlistNames = + playlistNames + |> List.map + (\name -> + { icon = Just (Icons.queue_music 16) + , title = name + , value = Alfred.StringValue name + } + ) + |> (\items -> + { name = Nothing, items = items } + ) + |> List.singleton diff --git a/src/Applications/UI/Ports.elm b/src/Applications/UI/Ports.elm index 5cf2a302..3ecf44ad 100644 --- a/src/Applications/UI/Ports.elm +++ b/src/Applications/UI/Ports.elm @@ -22,6 +22,9 @@ port copyToClipboard : String -> Cmd msg port loadAlbumCovers : { list : Bool, coverView : Bool } -> Cmd msg +port openUrlOnNewPage : String -> Cmd msg + + port pause : () -> Cmd msg diff --git a/src/Applications/UI/Routing/State.elm b/src/Applications/UI/Routing/State.elm index 4fdd7efa..b8b51227 100644 --- a/src/Applications/UI/Routing/State.elm +++ b/src/Applications/UI/Routing/State.elm @@ -1,4 +1,4 @@ -module UI.Routing.State exposing (linkClicked, resetUrl, transition, urlChanged) +module UI.Routing.State exposing (linkClicked, openUrlOnNewPage, resetUrl, transition, urlChanged) import Browser exposing (UrlRequest) import Browser.Navigation as Nav @@ -10,6 +10,7 @@ import Sources.Services.Dropbox import Sources.Services.Google import UI.Common.State as Common import UI.Page as Page exposing (Page) +import UI.Ports as Ports import UI.Sources.Form import UI.Sources.Page import UI.Sources.State as Sources @@ -44,6 +45,13 @@ linkClicked urlRequest model = return model (Nav.load href) +openUrlOnNewPage : String -> Manager +openUrlOnNewPage url model = + url + |> Ports.openUrlOnNewPage + |> return model + + urlChanged : Url -> Manager urlChanged url model = let diff --git a/src/Applications/UI/Tracks/State.elm b/src/Applications/UI/Tracks/State.elm index c1bd916e..9eeedfe8 100644 --- a/src/Applications/UI/Tracks/State.elm +++ b/src/Applications/UI/Tracks/State.elm @@ -103,6 +103,9 @@ update msg = Add a -> add a + AddFavourites a -> + addFavourites a + Reload a -> reload a @@ -112,6 +115,9 @@ update msg = RemoveBySourceId a -> removeBySourceId a + RemoveFavourites a -> + removeFavourites a + SortBy a -> sortBy a @@ -191,6 +197,11 @@ add encodedTracks model = model +addFavourites : List IdentifiedTrack -> Manager +addFavourites = + manageFavourites AddToFavourites + + changeScene : Scene -> Manager changeScene scene model = (case scene of @@ -379,6 +390,63 @@ insertCoverCache json model = |> Return.singleton +manageFavourites : FavouritesManagementAction -> List IdentifiedTrack -> Manager +manageFavourites action tracks model = + let + newFavourites = + (case action of + AddToFavourites -> + Favourites.completeFavouritesList + + RemoveFromFavourites -> + Favourites.removeFromFavouritesList + ) + tracks + model.favourites + + effect collection = + collection + |> Collection.map + (case action of + AddToFavourites -> + Favourites.completeTracksList tracks + + RemoveFromFavourites -> + Favourites.removeFromTracksList tracks + ) + |> (if model.favouritesOnly then + Collection.harvest + + else + identity + ) + + selectedCover = + Maybe.map + (\cover -> + cover.tracks + |> (case action of + AddToFavourites -> + Favourites.completeTracksList tracks + + RemoveFromFavourites -> + Favourites.removeFromTracksList tracks + ) + |> (\a -> { cover | tracks = a }) + ) + model.selectedCover + in + { model | favourites = newFavourites, selectedCover = selectedCover } + |> reviseCollection effect + |> andThen User.saveFavourites + |> (if model.scene == Covers then + andThen generateCovers >> andThen harvestCovers + + else + identity + ) + + markAsSelected : Int -> { shiftKey : Bool } -> Manager markAsSelected indexInList { shiftKey } model = let @@ -458,6 +526,11 @@ removeBySourceId sourceId model = |> andThen (removeFromCache removed) +removeFavourites : List IdentifiedTrack -> Manager +removeFavourites = + manageFavourites RemoveFromFavourites + + removeFromCache : List Track -> Manager removeFromCache tracks model = let @@ -976,3 +1049,12 @@ importHypaethral data selectedPlaylist model = Nothing -> andThen (Common.toggleLoadingScreen Off) ) + + + +-- ㊙️ + + +type FavouritesManagementAction + = AddToFavourites + | RemoveFromFavourites diff --git a/src/Applications/UI/Tracks/Types.elm b/src/Applications/UI/Tracks/Types.elm index 0b07fe92..e820d909 100644 --- a/src/Applications/UI/Tracks/Types.elm +++ b/src/Applications/UI/Tracks/Types.elm @@ -37,9 +37,11 @@ type Msg -- Collection ----------------------------------------- | Add Json.Value + | AddFavourites (List IdentifiedTrack) | Reload Json.Value | RemoveByPaths Json.Value | RemoveBySourceId String + | RemoveFavourites (List IdentifiedTrack) | SortBy SortBy | ToggleFavourite Int ----------------------------------------- diff --git a/src/Applications/UI/Types.elm b/src/Applications/UI/Types.elm index 7fb7e335..1df3aac6 100644 --- a/src/Applications/UI/Types.elm +++ b/src/Applications/UI/Types.elm @@ -261,6 +261,7 @@ type Msg ----------------------------------------- | ChangeUrlUsingPage Page | LinkClicked Browser.UrlRequest + | OpenUrlOnNewPage String | PageChanged Page | UrlChanged Url ----------------------------------------- diff --git a/src/Applications/UI/View.elm b/src/Applications/UI/View.elm index 5b398489..b9c61fd9 100644 --- a/src/Applications/UI/View.elm +++ b/src/Applications/UI/View.elm @@ -282,8 +282,8 @@ overlay maybeAlfred maybeContextMenu = brick [] [ "inset-0" - , "bg-black" - , "duration-1000" + , "bg-darkest-hour" + , "duration-500" , "ease-in-out" , "fixed" , "transition-opacity" @@ -291,7 +291,7 @@ overlay maybeAlfred maybeContextMenu = -- , ifThenElse isShown "pointer-events-auto" "pointer-events-none" - , ifThenElse isShown "opacity-40" "opacity-0" + , ifThenElse isShown "opacity-50" "opacity-0" ] [] diff --git a/src/Javascript/index.js b/src/Javascript/index.js index 69f6c17f..eb577699 100644 --- a/src/Javascript/index.js +++ b/src/Javascript/index.js @@ -81,6 +81,11 @@ function initialise(reg) { wire.covers() wire.webnative() + // Other ports + app.ports.openUrlOnNewPage.subscribe(url => { + window.open(url, "_blank") + }) + // Check for service worker updates every hour setInterval(() => reg.update(), 1 * 1000 * 60 * 60) } diff --git a/src/Library/Alfred.elm b/src/Library/Alfred.elm index 5bb43dbb..625e9383 100644 --- a/src/Library/Alfred.elm +++ b/src/Library/Alfred.elm @@ -1,20 +1,110 @@ module Alfred exposing (..) +import List.Extra as List +import Material.Icons.Types exposing (Coloring) +import Svg exposing (Svg) + + + -- 🌳 -type alias Alfred action = - { action : { result : Maybe String, searchTerm : Maybe String } -> List action +type alias Alfred msg = + { action : Action msg , focus : Int - , index : List String + , index : List (Group msg) + , indexFlattened : List (Item msg) , message : String , operation : Operation - , results : List String + , results : List (Group msg) , searchTerm : Maybe String } +type alias Action msg = + { result : Maybe (Item msg), searchTerm : Maybe String } -> List msg + + +type alias Group msg = + { name : Maybe String, items : List (Item msg) } + + +type alias Item msg = + { icon : Maybe (Coloring -> Svg msg) + , title : String + , value : ItemValue msg + } + + +type ItemValue msg + = Command msg + | StringValue String + + type Operation = Query | QueryOrMutation | Mutation + + + +-- 🛳 + + +create : + { action : Action msg + , index : List (Group msg) + , message : String + , operation : Operation + } + -> Alfred msg +create { action, index, message, operation } = + { action = action + , focus = 0 + , index = index + , indexFlattened = List.concatMap .items index + , message = message + , operation = operation + , results = index + , searchTerm = Nothing + } + + + +-- 🛠 + + +command : ItemValue msg -> Maybe msg +command val = + case val of + Command cmd -> + Just cmd + + StringValue _ -> + Nothing + + +stringValue : ItemValue msg -> Maybe String +stringValue val = + case val of + Command _ -> + Nothing + + StringValue string -> + Just string + + + +-- 🛠 + + +getAt : Int -> Alfred msg -> Maybe (Item msg) +getAt index alfred = + alfred.results + |> List.concatMap .items + |> List.getAt index + + +length : Alfred msg -> Int +length { indexFlattened } = + List.length indexFlattened diff --git a/src/Library/Tracks/Favourites.elm b/src/Library/Tracks/Favourites.elm index 9eb9f49c..38cb00d2 100644 --- a/src/Library/Tracks/Favourites.elm +++ b/src/Library/Tracks/Favourites.elm @@ -1,4 +1,4 @@ -module Tracks.Favourites exposing (match, simplified, toggleInFavouritesList, toggleInTracksList) +module Tracks.Favourites exposing (completeFavouritesList, completeTracksList, match, removeFromFavouritesList, removeFromTracksList, simplified, toggleInFavouritesList, toggleInTracksList) import List.Extra as List import Tracks exposing (Favourite, IdentifiedTrack, Track) @@ -8,6 +8,59 @@ import Tracks exposing (Favourite, IdentifiedTrack, Track) -- 🔱 +completeFavouritesList : List IdentifiedTrack -> List Favourite -> List Favourite +completeFavouritesList tracks favourites = + List.append + favourites + (List.filterMap + (\( i, t ) -> + if not i.isFavourite then + Just + { artist = t.tags.artist + , title = t.tags.title + } + + else + Nothing + ) + tracks + ) + + +completeTracksList : List IdentifiedTrack -> List IdentifiedTrack -> List IdentifiedTrack +completeTracksList tracksToMakeFavourite tracks = + let + favs = + List.filterMap + (\( i, t ) -> + if not i.isFavourite then + Just ( lowercaseArtist t, lowercaseTitle t ) + + else + Nothing + ) + tracksToMakeFavourite + in + List.map + (\( i, t ) -> + let + ( la, lt ) = + ( lowercaseArtist t, lowercaseTitle t ) + in + List.foldr + (\( lartist, ltitle ) ( ai, at ) -> + if la == lartist && lt == ltitle && not ai.isFavourite then + ( { ai | isFavourite = True }, at ) + + else + ( ai, at ) + ) + ( i, t ) + favs + ) + tracks + + match : Favourite -> Favourite -> Bool match a b = let @@ -24,6 +77,56 @@ match a b = aa == ba && at == bt +removeFromFavouritesList : List IdentifiedTrack -> List Favourite -> List Favourite +removeFromFavouritesList tracks favourites = + List.foldr + (\( i, t ) acc -> + if i.isFavourite then + List.filterNot + (match { artist = t.tags.artist, title = t.tags.title }) + acc + + else + acc + ) + favourites + tracks + + +removeFromTracksList : List IdentifiedTrack -> List IdentifiedTrack -> List IdentifiedTrack +removeFromTracksList tracksToRemoveFromFavs tracks = + let + unfavs = + List.filterMap + (\( i, t ) -> + if i.isFavourite then + Just ( lowercaseArtist t, lowercaseTitle t ) + + else + Nothing + ) + tracksToRemoveFromFavs + in + List.map + (\( i, t ) -> + let + ( la, lt ) = + ( lowercaseArtist t, lowercaseTitle t ) + in + List.foldr + (\( lartist, ltitle ) ( ai, at ) -> + if la == lartist && lt == ltitle && ai.isFavourite then + ( { ai | isFavourite = False }, at ) + + else + ( ai, at ) + ) + ( i, t ) + unfavs + ) + tracks + + simplified : Favourite -> String simplified fav = String.toLower fav.artist ++ String.toLower fav.title diff --git a/src/Static/About/Index.md b/src/Static/About/Index.md index 12c613b1..7516df17 100644 --- a/src/Static/About/Index.md +++ b/src/Static/About/Index.md @@ -96,28 +96,30 @@ artist:Parkway Drive - album:Atlas The app should be usable with only the keyboard, there are various keyboard shortcuts: -``` -L - Select playlist using autocompletion -N - Scroll to currently-playing track -P - Play / Pause -R - Toggle Repeat -S - Toggle Shuffle +```js +CTRL + K or CMD + K // Show command palette + +CTRL + L // Select playlist using autocompletion +CTRL + N // Scroll to currently-playing track +CTRL + P // Play / Pause +CTRL + R // Toggle Repeat +CTRL + S // Toggle Shuffle -{ / } - Previous / Next -< / > - Seek forwards / Seek backwards +CTRL + [ or ] // Previous / Next +CTRL + { or } // Seek forwards / Seek backwards -Alternatively you can use the media-control keys, +Alternatively you can use the media-control keys, if your browser supports it. -ESC - Close overlay, close context-menu, deselect album cover, etc. +ESC // Close overlay, close context-menu, deselect album cover, etc. -1 - Tracks -2 - Playlists -3 - Queue -4 - EQ +CTRL + 1 // Tracks +CTRL + 2 // Playlists +CTRL + 3 // Queue +CTRL + 4 // EQ -8 - Sources -9 - Settings +CTRL + 8 // Sources +CTRL + 9 // Settings ``` diff --git a/system/Css/Tailwind.js b/system/Css/Tailwind.js index 18328808..d8d0307b 100644 --- a/system/Css/Tailwind.js +++ b/system/Css/Tailwind.js @@ -205,13 +205,16 @@ module.exports = { "05": ".05", "10": ".1", "20": ".2", + "25": ".25", "30": ".3", "40": ".4", "50": ".5", "60": ".6", "70": ".7", + "75": ".75", "80": ".8", "90": ".9", + "95": ".95", "100": "1" },