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

Configure Feed

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

Implement search

+255 -24
+1 -1
Makefile
··· 29 29 elm: 30 30 @echo "> Compiling Elm application" 31 31 @elm make $(SRC_DIR)/Applications/Brain.elm --output $(BUILD_DIR)/brain.js 32 - @elm make $(SRC_DIR)/Applications/UI.elm --output $(BUILD_DIR)/application.js 32 + @elm make $(SRC_DIR)/Applications/UI.elm --output $(BUILD_DIR)/application.js --debug 33 33 34 34 35 35 system:
+5 -1
elm.json
··· 31 31 "justgage/tachyons-elm": "4.1.1", 32 32 "noahzgordon/elm-color-extra": "1.0.1", 33 33 "pilatch/flip": "1.0.0", 34 + "rluiten/elm-text-search": "5.0.0", 34 35 "rtfeldman/elm-css": "16.0.0", 35 36 "rtfeldman/elm-hex": "1.0.0", 36 37 "ryannhg/date-format": "2.3.0", ··· 43 44 "elm/random": "1.0.0", 44 45 "elm-explorations/test": "1.2.0", 45 46 "fredcy/elm-parseint": "2.0.1", 46 - "jinjor/elm-xml-parser": "2.0.0" 47 + "jinjor/elm-xml-parser": "2.0.0", 48 + "rluiten/sparsevector": "1.0.3", 49 + "rluiten/stemmer": "1.0.4", 50 + "rluiten/trie": "2.0.3" 47 51 } 48 52 }, 49 53 "test-dependencies": {
+53 -8
src/Applications/Brain.elm
··· 8 8 import Brain.Reply as Reply exposing (Reply(..)) 9 9 import Brain.Sources.Processing as Processing 10 10 import Brain.Sources.Processing.Common as Processing 11 + import Brain.Tracks as Tracks 11 12 import Json.Decode as Json 12 13 import Json.Decode.Pipeline exposing (optional) 13 14 import Json.Encode 14 - import Replying exposing (return) 15 + import Replying exposing (andThen, return) 15 16 import Sources.Encoding as Sources 16 17 import Sources.Processing.Encoding as Processing 17 18 import Tracks.Encoding as Tracks ··· 42 43 { authentication = Authentication.initialModel 43 44 , hypaethralUserData = Authentication.emptyHypaethralUserData 44 45 , processing = Processing.initialModel 46 + , tracks = Tracks.initialModel 45 47 } 46 48 ----------------------------------------- 47 49 -- Initial command ··· 85 87 ProcessingMsg sub -> 86 88 updateProcessing model sub 87 89 90 + TracksMsg sub -> 91 + updateTracks model sub 92 + 88 93 ----------------------------------------- 89 94 -- User data 90 95 ----------------------------------------- 96 + -- The hypaethral user data is received in pieces, 97 + -- pieces which are "cached" here in the web worker. 98 + -- 99 + -- The reasons for this are: 100 + -- 1. Lesser performance penalty on the UI when saving data 101 + -- (ie. this avoids having to encode/decode everything each time) 102 + -- 2. The data can be used in the web worker (brain) as well. 103 + -- (eg. for track-search index) 104 + -- 91 105 LoadHypaethralUserData value -> 92 106 let 93 107 decodedData = ··· 98 112 ( { model | hypaethralUserData = decodedData } 99 113 , Brain.Ports.toUI (Alien.broadcast Alien.LoadHypaethralUserData value) 100 114 ) 115 + |> andThen updateSearchIndex 101 116 102 117 SaveFavourites value -> 103 118 value ··· 118 133 |> Json.decodeValue (Json.list Tracks.trackDecoder) 119 134 |> Result.withDefault model.hypaethralUserData.tracks 120 135 |> hypaethralLenses.setTracks model 121 - |> saveHypaethralData 136 + |> updateSearchIndex 137 + |> andThen saveHypaethralData 122 138 123 139 140 + updateSearchIndex : Model -> ( Model, Cmd Msg ) 141 + updateSearchIndex model = 142 + update 143 + (model.hypaethralUserData.tracks 144 + |> Tracks.UpdateSearchIndex 145 + |> TracksMsg 146 + ) 147 + model 124 148 125 - -- 📣 ░░ CHILDREN & REPLIES 149 + 150 + 151 + -- 📣 ░░ REPLIES 126 152 127 153 128 154 translateReply : Reply -> Msg ··· 148 174 Replying.updateChild update translateReply 149 175 150 176 177 + 178 + -- 📣 ░░ CHILDREN 179 + 180 + 151 181 updateAuthentication : Model -> Authentication.Msg -> ( Model, Cmd Msg ) 152 182 updateAuthentication model sub = 153 183 updateChild ··· 172 202 } 173 203 174 204 205 + updateTracks : Model -> Tracks.Msg -> ( Model, Cmd Msg ) 206 + updateTracks model sub = 207 + updateChild 208 + { mapCmd = TracksMsg 209 + , mapModel = \child -> { model | tracks = child } 210 + , update = Tracks.update 211 + } 212 + { model = model.tracks 213 + , msg = sub 214 + } 215 + 216 + 175 217 176 218 -- 📣 ░░ USER DATA 177 219 ··· 185 227 186 228 makeHypaethralLens : (HypaethralUserData -> a -> HypaethralUserData) -> Model -> a -> Model 187 229 makeHypaethralLens setter model value = 188 - let 189 - h = 190 - model.hypaethralUserData 191 - in 192 - { model | hypaethralUserData = setter h value } 230 + { model | hypaethralUserData = setter model.hypaethralUserData value } 193 231 194 232 195 233 saveHypaethralData : Model -> ( Model, Cmd Msg ) ··· 260 298 261 299 Just Alien.SaveTracks -> 262 300 SaveTracks event.data 301 + 302 + Just Alien.SearchTracks -> 303 + event.data 304 + |> Json.decodeValue Json.string 305 + |> Result.withDefault "" 306 + |> Tracks.Search 307 + |> TracksMsg 263 308 264 309 Just Alien.SignIn -> 265 310 AuthenticationMsg (Authentication.PerformSignIn event.data)
+3
src/Applications/Brain/Core.elm
··· 4 4 import Authentication 5 5 import Brain.Authentication as Authentication 6 6 import Brain.Sources.Processing.Common as Processing 7 + import Brain.Tracks as Tracks 7 8 import Json.Decode as Json 8 9 9 10 ··· 23 24 { authentication : Authentication.Model 24 25 , hypaethralUserData : Authentication.HypaethralUserData 25 26 , processing : Processing.Model 27 + , tracks : Tracks.Model 26 28 } 27 29 28 30 ··· 38 40 ----------------------------------------- 39 41 | AuthenticationMsg Authentication.Msg 40 42 | ProcessingMsg Processing.Msg 43 + | TracksMsg Tracks.Msg 41 44 ----------------------------------------- 42 45 -- User data 43 46 -----------------------------------------
+102
src/Applications/Brain/Tracks.elm
··· 1 + module Brain.Tracks exposing (IndexedTrack, Model, Msg(..), createSearchIndex, initialModel, update) 2 + 3 + import Alien 4 + import Brain.Reply exposing (Reply(..)) 5 + import ElmTextSearch 6 + import Json.Encode as Json 7 + import Replying exposing (R3D3) 8 + import Tracks exposing (Track) 9 + 10 + 11 + 12 + -- 🌳 13 + 14 + 15 + type alias Model = 16 + { searchIndex : ElmTextSearch.Index IndexedTrack 17 + } 18 + 19 + 20 + type alias IndexedTrack = 21 + { ref : String 22 + 23 + -- 24 + , album : String 25 + , artist : String 26 + , title : String 27 + } 28 + 29 + 30 + initialModel : Model 31 + initialModel = 32 + { searchIndex = createSearchIndex [] 33 + } 34 + 35 + 36 + 37 + -- 📣 38 + 39 + 40 + type Msg 41 + = Search String 42 + | UpdateSearchIndex (List Track) 43 + 44 + 45 + update : Msg -> Model -> R3D3 Model Msg Reply 46 + update msg model = 47 + case msg of 48 + Search term -> 49 + let 50 + ( updatedIndex, results ) = 51 + Result.withDefault 52 + ( model.searchIndex, [] ) 53 + (ElmTextSearch.search 54 + term 55 + model.searchIndex 56 + ) 57 + 58 + json = 59 + results 60 + |> List.map Tuple.first 61 + |> Json.list Json.string 62 + in 63 + ( { model | searchIndex = updatedIndex } 64 + , Cmd.none 65 + , Just [ GiveUI Alien.SearchTracks json ] 66 + ) 67 + 68 + UpdateSearchIndex tracks -> 69 + ( { model | searchIndex = createSearchIndex tracks } 70 + , Cmd.none 71 + , Nothing 72 + ) 73 + 74 + 75 + 76 + -- SEARCH 77 + 78 + 79 + createSearchIndex : List Track -> ElmTextSearch.Index IndexedTrack 80 + createSearchIndex tracks = 81 + { ref = .ref 82 + , fields = 83 + [ ( .album, 5.0 ) 84 + , ( .artist, 5.0 ) 85 + , ( .title, 5.0 ) 86 + ] 87 + , listFields = [] 88 + } 89 + |> ElmTextSearch.new 90 + |> ElmTextSearch.addDocs (List.map makeIndexedTrack tracks) 91 + |> Tuple.first 92 + 93 + 94 + makeIndexedTrack : Track -> IndexedTrack 95 + makeIndexedTrack track = 96 + { ref = track.id 97 + 98 + -- 99 + , artist = track.tags.artist 100 + , album = track.tags.album 101 + , title = track.tags.title 102 + }
+12
src/Applications/UI.elm
··· 283 283 Reply.SaveTracks -> 284 284 Core.SaveTracks 285 285 286 + ----------------------------------------- 287 + -- To Brain 288 + ----------------------------------------- 289 + GiveBrain tag data -> 290 + NotifyBrain (Alien.broadcast tag data) 291 + 292 + NudgeBrain tag -> 293 + NotifyBrain (Alien.trigger tag) 294 + 286 295 287 296 updateChild = 288 297 Replying.updateChild update translateReply ··· 333 342 Just Alien.ReportProcessingError -> 334 343 -- TODO 335 344 Bypass 345 + 346 + Just Alien.SearchTracks -> 347 + TracksMsg (UI.Tracks.SetSearchResults event.data) 336 348 337 349 Just Alien.UpdateSourceData -> 338 350 -- TODO
+3
src/Applications/UI/Reply.elm
··· 19 19 | SaveFavourites 20 20 | SaveSources 21 21 | SaveTracks 22 + -- Brain 23 + | GiveBrain Alien.Tag Json.Value 24 + | NudgeBrain Alien.Tag
+46 -9
src/Applications/UI/Tracks.elm
··· 1 1 module UI.Tracks exposing (Model, Msg(..), initialModel, makeParcel, resolveParcel, update, view) 2 2 3 + import Alien 3 4 import Chunky exposing (..) 4 5 import Color 5 6 import Color.Ext as Color 6 7 import Css 7 8 import Html.Styled as Html exposing (Html, text) 8 9 import Html.Styled.Attributes exposing (css, placeholder, title, value) 9 - import Html.Styled.Events exposing (onClick, onInput) 10 + import Html.Styled.Events exposing (onBlur, onClick, onInput) 10 11 import Html.Styled.Ext exposing (onEnterKey) 11 12 import Html.Styled.Lazy exposing (..) 12 - import Json.Decode 13 + import Json.Decode as Json 14 + import Json.Encode 13 15 import Material.Icons.Action as Icons 14 16 import Material.Icons.Av as Icons 15 17 import Material.Icons.Content as Icons 16 18 import Material.Icons.Editor as Icons 17 19 import Replying exposing (R3D3) 20 + import Return2 18 21 import Return3 19 22 import Tachyons.Classes as T 20 23 import Tracks exposing (..) ··· 65 68 ----------------------------------------- 66 69 -- Collection 67 70 ----------------------------------------- 68 - | Add Json.Decode.Value 71 + | Add Json.Value 69 72 ----------------------------------------- 70 73 -- Favourites 71 74 ----------------------------------------- ··· 75 78 ----------------------------------------- 76 79 | ClearSearch 77 80 | Search (Maybe String) 81 + | SetSearchResults Json.Value 78 82 | SetSearchTerm String 79 83 80 84 ··· 94 98 let 95 99 tracks = 96 100 json 97 - |> Json.Decode.decodeValue (Json.Decode.list Encoding.trackDecoder) 101 + |> Json.decodeValue (Json.list Encoding.trackDecoder) 98 102 |> Result.withDefault [] 99 103 in 100 104 model ··· 112 116 -- Search 113 117 ----------------------------------------- 114 118 ClearSearch -> 115 - Return3.withNothing { model | searchTerm = Nothing } 119 + reviseCollection 120 + harvest 121 + { model | searchResults = Nothing, searchTerm = Nothing } 122 + 123 + Search Nothing -> 124 + reviseCollection 125 + harvest 126 + { model | searchResults = Nothing, searchTerm = Nothing } 127 + 128 + Search (Just "") -> 129 + reviseCollection 130 + harvest 131 + { model | searchResults = Nothing, searchTerm = Nothing } 132 + 133 + Search (Just term) -> 134 + { model | searchTerm = Just term } 135 + |> Return2.withNoCmd 136 + |> Return3.withReply [ GiveBrain Alien.SearchTracks (Json.Encode.string term) ] 116 137 117 - Search maybeTerm -> 118 - Return3.withNothing { model | searchTerm = maybeTerm } 138 + SetSearchResults json -> 139 + json 140 + |> Json.decodeValue (Json.list Json.string) 141 + |> Result.withDefault [] 142 + |> (\results -> { model | searchResults = Just results }) 143 + |> reviseCollection harvest 144 + 145 + SetSearchTerm "" -> 146 + Return3.withNothing { model | searchTerm = Nothing } 119 147 120 148 SetSearchTerm term -> 121 149 Return3.withNothing { model | searchTerm = Just term } ··· 155 183 Return3.withNothing modelWithNewCollection 156 184 157 185 186 + reviseCollection : (Parcel -> Parcel) -> Model -> R3D3 Model Msg Reply 187 + reviseCollection collector model = 188 + model 189 + |> makeParcel 190 + |> collector 191 + |> resolveParcel model 192 + 193 + 158 194 159 195 -- 🗺 160 196 ··· 172 208 , chunk 173 209 [] 174 210 (List.map 175 - (\t -> text <| t.tags.artist ++ " - " ++ t.tags.title) 176 - model.collection.untouched 211 + (\( _, t ) -> text <| t.tags.artist ++ " - " ++ t.tags.title) 212 + model.collection.harvested 177 213 ) 178 214 ] 179 215 ··· 196 232 slab 197 233 Html.input 198 234 [ css searchInputStyles 235 + , onBlur (Search searchTerm) 199 236 , onEnterKey (Search searchTerm) 200 237 , onInput SetSearchTerm 201 238 , placeholder "Search"
+2
src/Applications/UI/UserData.elm
··· 5 5 import Json.Decode.Pipeline exposing (..) 6 6 import Json.Encode 7 7 import Replying exposing (R3D3) 8 + import Sources 8 9 import Sources.Encoding as Sources 9 10 import Tracks exposing (emptyCollection) 10 11 import Tracks.Collection as Tracks ··· 81 82 adjustedModel = 82 83 { model 83 84 | collection = { emptyCollection | untouched = data.tracks } 85 + , enabledSourceIds = Sources.enabledSourceIds data.sources 84 86 , favourites = data.favourites 85 87 } 86 88 in
+7
src/Library/Alien.elm
··· 22 22 = AuthAnonymous 23 23 | AuthEnclosedData 24 24 | AuthMethod 25 + | SearchTracks 25 26 -- from UI 26 27 | ProcessSources 27 28 | SaveEnclosedUserData ··· 81 82 82 83 AuthEnclosedData -> 83 84 "AUTH_ENCLOSED_DATA" 85 + 86 + SearchTracks -> 87 + "SEARCH_TRACKS" 84 88 85 89 ----------------------------------------- 86 90 -- From UI ··· 148 152 149 153 "AUTH_ENCLOSED_DATA" -> 150 154 Just AuthEnclosedData 155 + 156 + "SEARCH_TRACKS" -> 157 + Just SearchTracks 151 158 152 159 ----------------------------------------- 153 160 -- From UI
+11 -1
src/Library/Replying.elm
··· 1 - module Replying exposing (R3D3, Updator, do, reducto, return, updateChild) 1 + module Replying exposing (R3D3, Updator, andThen, do, reducto, return, updateChild) 2 2 3 + import Return2 3 4 import Return3 4 5 import Task 5 6 ··· 48 49 return : model -> Cmd msg -> ( model, Cmd msg ) 49 50 return model msg = 50 51 ( model, msg ) 52 + 53 + 54 + {-| Chain `update` calls. 55 + -} 56 + andThen : (model -> ( model, Cmd msg )) -> ( model, Cmd msg ) -> ( model, Cmd msg ) 57 + andThen fn ( model, cmd ) = 58 + model 59 + |> fn 60 + |> Return2.addCmd cmd 51 61 52 62 53 63 {-| Reduce a `R3D3` to a `R2D2`.
+7 -1
src/Library/Sources.elm
··· 1 - module Sources exposing (Page(..), Property, Service(..), Source, SourceData, setProperId) 1 + module Sources exposing (Page(..), Property, Service(..), Source, SourceData, enabledSourceIds, setProperId) 2 2 3 + import Conditional exposing (..) 3 4 import Dict exposing (Dict) 4 5 import Time 5 6 ··· 52 53 53 54 54 55 --- 🔱 56 + 57 + 58 + enabledSourceIds : List Source -> List String 59 + enabledSourceIds = 60 + List.filterMap (\s -> ifThenElse s.enabled (Just s.id) Nothing) 55 61 56 62 57 63 setProperId : Int -> Time.Posix -> Source -> Source
+2 -2
src/Library/String/Ext.elm
··· 5 5 chopEnd needle str = 6 6 if String.endsWith needle str then 7 7 str 8 - |> String.dropRight (String.length str) 8 + |> String.dropRight 1 9 9 |> chopEnd needle 10 10 11 11 else ··· 16 16 chopStart needle str = 17 17 if String.startsWith needle str then 18 18 str 19 - |> String.dropLeft (String.length str) 19 + |> String.dropLeft 1 20 20 |> chopStart needle 21 21 22 22 else
+1 -1
src/README.md
··· 6 6 - Applications/UI 7 7 - Library 8 8 9 - `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. 9 + `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. 10 10 11 11 12 12