diff --git a/elm.json b/elm.json index ce6ff913..2add2916 100644 --- a/elm.json +++ b/elm.json @@ -29,6 +29,7 @@ "jorgengranseth/elm-string-format": "1.0.1", "justgage/tachyons-elm": "4.1.1", "noahzgordon/elm-color-extra": "1.0.1", + "pilatch/flip": "1.0.0", "rtfeldman/elm-css": "16.0.0", "rtfeldman/elm-hex": "1.0.0", "ryannhg/date-format": "2.3.0", @@ -48,4 +49,4 @@ "direct": {}, "indirect": {} } -} +} \ No newline at end of file diff --git a/src/Applications/Brain.elm b/src/Applications/Brain.elm index 4186f3c9..f630c542 100644 --- a/src/Applications/Brain.elm +++ b/src/Applications/Brain.elm @@ -89,7 +89,7 @@ update msg model = --- 📣 ░░░ CHILDREN & REPLIES +-- 📣 ░░ CHILDREN & REPLIES translateReply : Reply -> Msg diff --git a/src/Applications/UI.elm b/src/Applications/UI.elm index 8b137bdd..7f9222d2 100644 --- a/src/Applications/UI.elm +++ b/src/Applications/UI.elm @@ -31,6 +31,7 @@ import UI.Reply as Reply exposing (Reply(..)) import UI.Settings import UI.Sources import UI.Svg.Elements +import UI.Tracks import UI.UserData import Url exposing (Url) @@ -69,6 +70,7 @@ init flags url key = -- Children , backdrop = UI.Backdrop.initialModel , sources = UI.Sources.initialModel + , tracks = UI.Tracks.initialModel } ----------------------------------------- -- Initial command @@ -133,6 +135,16 @@ update msg model = , msg = sub } + TracksMsg sub -> + updateChild + { mapCmd = TracksMsg + , mapModel = \child -> { model | tracks = child } + , update = UI.Tracks.update + } + { model = model.tracks + , msg = sub + } + ----------------------------------------- -- Brain ----------------------------------------- @@ -211,7 +223,7 @@ update msg model = --- 📣 ░░░ CHILDREN & REPLIES +-- 📣 ░░ CHILDREN & REPLIES translateReply : Reply -> Msg @@ -352,7 +364,9 @@ defaultScreen model = ----------------------------------------- , case model.page of Page.Index -> - empty + model.tracks + |> Lazy.lazy UI.Tracks.view + |> Html.map TracksMsg Page.NotFound -> text "Page not found." @@ -375,7 +389,7 @@ defaultScreen model = --- 🗺 ░░░ BITS +-- 🗺 ░░ BITS content : List (Html msg) -> Html msg @@ -398,7 +412,7 @@ loadingAnimation = --- 🖼 ░░░ GLOBAL +-- 🖼 ░░ GLOBAL globalCss : List Css.Global.Snippet diff --git a/src/Applications/UI/Core.elm b/src/Applications/UI/Core.elm index 2fc1ceb5..56c1f957 100644 --- a/src/Applications/UI/Core.elm +++ b/src/Applications/UI/Core.elm @@ -8,6 +8,7 @@ import Json.Encode as Json import UI.Backdrop import UI.Page exposing (Page) import UI.Sources +import UI.Tracks import Url exposing (Url) @@ -35,6 +36,7 @@ type alias Model = ----------------------------------------- , backdrop : UI.Backdrop.Model , sources : UI.Sources.Model + , tracks : UI.Tracks.Model } @@ -52,6 +54,7 @@ type Msg ----------------------------------------- | BackdropMsg UI.Backdrop.Msg | SourcesMsg UI.Sources.Msg + | TracksMsg UI.Tracks.Msg ----------------------------------------- -- Brain ----------------------------------------- diff --git a/src/Applications/UI/Tracks.elm b/src/Applications/UI/Tracks.elm new file mode 100644 index 00000000..afe4989c --- /dev/null +++ b/src/Applications/UI/Tracks.elm @@ -0,0 +1,48 @@ +module UI.Tracks exposing (Model, Msg(..), initialModel, update, view) + +import Chunky exposing (..) +import Html.Styled as Html exposing (Html, text) +import Replying exposing (R3D3) +import Return3 +import Tracks exposing (..) +import UI.Kit +import UI.Reply exposing (Reply) + + + +-- 🌳 + + +type alias Model = + { collection : Collection + } + + +initialModel : Model +initialModel = + { collection = emptyCollection + } + + + +-- 📣 + + +type Msg + = Bypass + + +update : Msg -> Model -> R3D3 Model Msg Reply +update msg model = + case msg of + Bypass -> + Return3.withNothing model + + + +-- 🗺 + + +view : Model -> Html Msg +view model = + UI.Kit.vessel [] diff --git a/src/Applications/UI/UserData.elm b/src/Applications/UI/UserData.elm index 1d3d0186..d31c6e28 100644 --- a/src/Applications/UI/UserData.elm +++ b/src/Applications/UI/UserData.elm @@ -36,7 +36,7 @@ importHypaethral value model = --- 📭 ░░░ IMPORTING HYPAETHRAL +-- 📭 ░░ IMPORTING HYPAETHRAL importSources : UI.Sources.Model -> HypaethralBundle -> UI.Sources.Model @@ -45,7 +45,7 @@ importSources model ( data, _ ) = --- 📭 ░░░ DECODING +-- 📭 ░░ DECODING decode : Decode.Value -> Result Decode.Error HypaethralUserData @@ -61,7 +61,7 @@ decoder = --- 📭 ░░░ FALLBACKS +-- 📭 ░░ FALLBACKS emptyHypaethralUserData : HypaethralUserData @@ -81,7 +81,7 @@ exportHypaethral = --- 📮 ░░░ ENCODING +-- 📮 ░░ ENCODING encode : UI.Core.Model -> Encode.Value diff --git a/src/Library/Replying.elm b/src/Library/Replying.elm index 0cfa2556..e69882eb 100644 --- a/src/Library/Replying.elm +++ b/src/Library/Replying.elm @@ -47,7 +47,7 @@ return model msg = --- 🔱 ░░░ TASKS +-- 🔱 ░░ TASKS do : msg -> Cmd msg diff --git a/src/Library/Tracks.elm b/src/Library/Tracks.elm index 50bc341b..ca9aeed2 100644 --- a/src/Library/Tracks.elm +++ b/src/Library/Tracks.elm @@ -1,4 +1,4 @@ -module Tracks exposing (Collection, Favourite, IdentifiedTrack, Identifiers, Tags, Track, emptyCollection, emptyIdentifiedTrack, emptyTags, emptyTrack, makeTrack, missingId) +module Tracks exposing (Collection, Favourite, IdentifiedTrack, Identifiers, Parcel, SortBy(..), SortDirection(..), Tags, Track, emptyCollection, emptyIdentifiedTrack, emptyTags, emptyTrack, makeTrack, missingId) import Base64 import Bytes.Encode @@ -81,6 +81,37 @@ type alias Collection = } +type alias Parcel = + ( -- Things I need to make a proper collection: + { enabledSourceIds : List String + , favourites : List Favourite + , favouritesOnly : Bool + , nowPlaying : Maybe IdentifiedTrack + , searchResults : Maybe (List String) + , sortBy : SortBy + , sortDirection : SortDirection + } + -- The collection itself: + , Collection + ) + + + +-- SORTING + + +type SortBy + = Artist + | Album + | PlaylistIndex + | Title + + +type SortDirection + = Asc + | Desc + + -- 🔱 diff --git a/src/Library/Tracks/Collection/Internal.elm b/src/Library/Tracks/Collection/Internal.elm new file mode 100644 index 00000000..d6b38b4a --- /dev/null +++ b/src/Library/Tracks/Collection/Internal.elm @@ -0,0 +1,58 @@ +module Tracks.Collection.Internal exposing + ( arrange + , build + , buildf + , harvest + , identify + , initialize + ) + +import Flip exposing (flip) +import Tracks exposing (Parcel, Track) +import Tracks.Collection.Internal.Arrange as Internal +import Tracks.Collection.Internal.Harvest as Internal +import Tracks.Collection.Internal.Identify as Internal + + + +-- 🔱 + + +build : List Track -> Parcel -> Parcel +build tracks = + initialize tracks >> identify >> arrange >> harvest + + +buildf : Parcel -> List Track -> Parcel +buildf = + flip build + + + +-- INITIALIZE + + +initialize : List Track -> Parcel -> Parcel +initialize tracks ( dependencies, collection ) = + ( dependencies + , { collection | untouched = tracks } + ) + + + +-- RE-EXPORT + + +identify : Parcel -> Parcel +identify = + Internal.identify + + +arrange : Parcel -> Parcel +arrange = + Internal.arrange + + +harvest : Parcel -> Parcel +harvest = + Internal.harvest diff --git a/src/Library/Tracks/Collection/Internal/Arrange.elm b/src/Library/Tracks/Collection/Internal/Arrange.elm new file mode 100644 index 00000000..7f15aa92 --- /dev/null +++ b/src/Library/Tracks/Collection/Internal/Arrange.elm @@ -0,0 +1,16 @@ +module Tracks.Collection.Internal.Arrange exposing (arrange) + +import Tracks exposing (..) +import Tracks.Sorting as Sorting + + + +-- 🍯 + + +arrange : Parcel -> Parcel +arrange ( deps, collection ) = + collection.identified + |> Sorting.sort deps.sortBy deps.sortDirection + |> (\x -> { collection | arranged = x }) + |> (\x -> ( deps, x )) diff --git a/src/Library/Tracks/Collection/Internal/Harvest.elm b/src/Library/Tracks/Collection/Internal/Harvest.elm new file mode 100644 index 00000000..89d5ae36 --- /dev/null +++ b/src/Library/Tracks/Collection/Internal/Harvest.elm @@ -0,0 +1,71 @@ +module Tracks.Collection.Internal.Harvest exposing (harvest) + +import List.Extra as List +import Maybe.Extra as Maybe +import Tracks exposing (..) + + + +-- 🍯 + + +harvest : Parcel -> Parcel +harvest ( deps, collection ) = + let + harvested = + case deps.searchResults of + Just [] -> + [] + + Just trackIds -> + collection.arranged + |> List.foldl harvester ( [], trackIds ) + |> Tuple.first + + Nothing -> + collection.arranged + + filters = + [ -- Favourites / Missing + ----------------------- + if deps.favouritesOnly then + Tuple.first >> .isFavourite >> (==) True + + else + Tuple.first >> .isMissing >> (==) False + ] + + theFilter x = + List.foldl + (\filter bool -> + if bool == True then + filter x + + else + bool + ) + True + filters + in + harvested + |> List.filter theFilter + |> List.indexedMap (\idx tup -> Tuple.mapFirst (\i -> { i | indexInList = idx }) tup) + |> (\h -> { collection | harvested = h }) + |> (\c -> ( deps, c )) + + +harvester : + IdentifiedTrack + -> ( List IdentifiedTrack, List String ) + -> ( List IdentifiedTrack, List String ) +harvester ( i, t ) ( acc, trackIds ) = + case List.findIndex ((==) t.id) trackIds of + Just idx -> + ( acc ++ [ ( i, t ) ] + , List.removeAt idx trackIds + ) + + Nothing -> + ( acc + , trackIds + ) diff --git a/src/Library/Tracks/Collection/Internal/Identify.elm b/src/Library/Tracks/Collection/Internal/Identify.elm new file mode 100644 index 00000000..669db8c1 --- /dev/null +++ b/src/Library/Tracks/Collection/Internal/Identify.elm @@ -0,0 +1,140 @@ +module Tracks.Collection.Internal.Identify exposing (identify) + +import List.Extra as List +import Tracks exposing (..) +import Tracks.Favourites as Favourites + + + +-- 🔱 + + +identify : Parcel -> Parcel +identify ( deps, collection ) = + let + ( identifiedUnsorted, missingFavourites ) = + List.foldl + (identifyTrack + deps.enabledSourceIds + deps.favourites + deps.nowPlaying + ) + ( [], deps.favourites ) + collection.untouched + in + identifiedUnsorted + |> List.append (List.map makeMissingFavouriteTrack missingFavourites) + |> (\x -> { collection | identified = x }) + |> (\x -> ( deps, x )) + + + +-- IDENTIFY + + +identifyTrack : + List String + -> List Favourite + -> Maybe IdentifiedTrack + -> Track + -> ( List IdentifiedTrack, List Favourite ) + -> ( List IdentifiedTrack, List Favourite ) +identifyTrack enabledSourceIds favourites nowPlaying track = + case List.member track.sourceId enabledSourceIds of + True -> + partTwo favourites nowPlaying track + + False -> + identity + + +partTwo : + List Favourite + -> Maybe IdentifiedTrack + -> Track + -> ( List IdentifiedTrack, List Favourite ) + -> ( List IdentifiedTrack, List Favourite ) +partTwo favourites nowPlaying track ( acc, remainingFavourites ) = + let + isNP = + nowPlaying + |> Maybe.map (Tuple.second >> .id >> (==) track.id) + |> Maybe.withDefault False + + isFavourite_ = + isFavourite track + + isFav = + List.any isFavourite_ favourites + + identifiedTrack = + ( { indexInList = 0 + , indexInPlaylist = Nothing + , isFavourite = isFav + , isMissing = False + , isNowPlaying = isNP + , isSelected = False + } + , track + ) + in + case isFav of + -- + -- A favourite + -- + True -> + ( identifiedTrack :: acc + , remainingFavourites + |> List.findIndex isFavourite_ + |> Maybe.map (\idx -> List.removeAt idx remainingFavourites) + |> Maybe.withDefault remainingFavourites + ) + + -- + -- Not a favourite + -- + False -> + ( identifiedTrack :: acc + , remainingFavourites + ) + + + +-- FAVOURITES + + +isFavourite : Track -> (Favourite -> Bool) +isFavourite track = + Favourites.match + { artist = track.tags.artist + , title = track.tags.title + } + + +makeMissingFavouriteTrack : Favourite -> IdentifiedTrack +makeMissingFavouriteTrack fav = + let + tags = + { disc = 1 + , nr = 0 + , artist = fav.artist + , title = fav.title + , album = missingId + , genre = Nothing + , picture = Nothing + , year = Nothing + } + in + ( { indexInList = 0 + , indexInPlaylist = Nothing + , isFavourite = True + , isMissing = True + , isNowPlaying = False + , isSelected = False + } + , { tags = tags + , id = missingId + , path = missingId + , sourceId = missingId + } + ) diff --git a/src/Library/Tracks/Favourites.elm b/src/Library/Tracks/Favourites.elm new file mode 100644 index 00000000..9226d53e --- /dev/null +++ b/src/Library/Tracks/Favourites.elm @@ -0,0 +1,23 @@ +module Tracks.Favourites exposing (match) + +import Tracks exposing (Favourite, Track) + + + +-- 🔱 + + +match : Favourite -> Favourite -> Bool +match a b = + let + ( aa, at ) = + ( String.toLower a.artist + , String.toLower a.title + ) + + ( ba, bt ) = + ( String.toLower b.artist + , String.toLower b.title + ) + in + aa == ba && at == bt diff --git a/src/Library/Tracks/Sorting.elm b/src/Library/Tracks/Sorting.elm new file mode 100644 index 00000000..4407747c --- /dev/null +++ b/src/Library/Tracks/Sorting.elm @@ -0,0 +1,120 @@ +module Tracks.Sorting exposing (sort) + +import Tracks exposing (..) + + + +-- 🔱 + + +sort : SortBy -> SortDirection -> List IdentifiedTrack -> List IdentifiedTrack +sort property direction list = + let + sortFn = + case property of + Album -> + sortByAlbum + + Artist -> + sortByArtist + + PlaylistIndex -> + sortByPlaylistIndex + + Title -> + sortByTitle + + dirFn = + if direction == Desc then + List.reverse + + else + identity + in + list + |> List.sortWith sortFn + |> dirFn + + + +-- BY + + +sortByAlbum : IdentifiedTrack -> IdentifiedTrack -> Order +sortByAlbum ( _, a ) ( _, b ) = + EQ + |> andThenCompare album a b + |> andThenCompare disc a b + |> andThenCompare nr a b + |> andThenCompare artist a b + |> andThenCompare title a b + + +sortByArtist : IdentifiedTrack -> IdentifiedTrack -> Order +sortByArtist ( _, a ) ( _, b ) = + EQ + |> andThenCompare artist a b + |> andThenCompare album a b + |> andThenCompare disc a b + |> andThenCompare nr a b + |> andThenCompare title a b + + +sortByTitle : IdentifiedTrack -> IdentifiedTrack -> Order +sortByTitle ( _, a ) ( _, b ) = + EQ + |> andThenCompare title a b + |> andThenCompare artist a b + |> andThenCompare album a b + + +sortByPlaylistIndex : IdentifiedTrack -> IdentifiedTrack -> Order +sortByPlaylistIndex ( a, _ ) ( b, _ ) = + andThenCompare (.indexInPlaylist >> Maybe.withDefault 0) a b EQ + + + +-- TAGS + + +album : Track -> String +album = + .tags >> .album >> low + + +artist : Track -> String +artist = + .tags >> .artist >> low + + +title : Track -> String +title = + .tags >> .title >> low + + +disc : Track -> Int +disc = + .tags >> .disc + + +nr : Track -> Int +nr = + .tags >> .nr + + + +-- COMMON + + +andThenCompare : (ctx -> comparable) -> ctx -> ctx -> Order -> Order +andThenCompare fn a b order = + if order == EQ then + compare (fn a) (fn b) + + else + order + + +low : String -> String +low = + String.toLower diff --git a/src/README.md b/src/README.md index 71ed680d..9d725624 100644 --- a/src/README.md +++ b/src/README.md @@ -6,7 +6,7 @@ Elm directories: - Applications/UI - Library -`UI` is the Elm application that'll be executed on the main thread (ie. the UI thread) and `Brain` is the Elm application that'll live inside a web worker. `UI` will be the main application and `Brain` does the heavy lifting. The code shared between these two applications lives in `Library`. +`UI` is the Elm application that'll be executed on the main thread (ie. the UI thread) and `Brain` is the Elm application that'll live inside a web worker. `UI` will be the main application and `Brain` does the heavy lifting. The code shared between these two applications lives in `Library`. The library also contains the more "generic", code that's not necessarily tied to one or the other.