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.

Add tracks table

+424 -89
+1
elm.json
··· 8 8 "dependencies": { 9 9 "direct": { 10 10 "Chadtech/return": "1.0.2", 11 + "FabienHenon/elm-infinite-list-view": "3.0.0", 11 12 "NoRedInk/elm-json-decode-pipeline": "1.0.0", 12 13 "avh4/elm-color": "1.0.0", 13 14 "danfishgold/base64-bytes": "1.0.1",
+5
package-lock.json
··· 7 7 "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz", 8 8 "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==" 9 9 }, 10 + "lunr": { 11 + "version": "2.3.5", 12 + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.5.tgz", 13 + "integrity": "sha512-EtnfmHsHJTr3u24sito9JctSxej5Ds0SgUD2Lm+qRHyLgM7BGesFlW14eNh1mil0fV5Muh8gf3dBBXzADlUlzQ==" 14 + }, 10 15 "tachyons": { 11 16 "version": "4.11.1", 12 17 "resolved": "https://registry.npmjs.org/tachyons/-/tachyons-4.11.1.tgz",
+1
package.json
··· 2 2 "private": true, 3 3 "dependencies": { 4 4 "fast-text-encoding": "~1.0.0", 5 + "lunr": "^2.3.5", 5 6 "tachyons": "~4.11.1" 6 7 } 7 8 }
+13 -8
src/Applications/Brain.elm
··· 108 108 value 109 109 |> Authentication.decodeHypaethral 110 110 |> Result.withDefault model.hypaethralUserData 111 + 112 + encodedTracks = 113 + Json.Encode.list Tracks.encodeTrack decodedData.tracks 111 114 in 112 - ( { model | hypaethralUserData = decodedData } 113 - , Brain.Ports.toUI (Alien.broadcast Alien.LoadHypaethralUserData value) 114 - ) 115 - |> andThen updateSearchIndex 115 + andThen 116 + (updateSearchIndex encodedTracks) 117 + ( { model | hypaethralUserData = decodedData } 118 + , Brain.Ports.toUI (Alien.broadcast Alien.LoadHypaethralUserData value) 119 + ) 116 120 117 121 SaveFavourites value -> 118 122 value ··· 133 137 |> Json.decodeValue (Json.list Tracks.trackDecoder) 134 138 |> Result.withDefault model.hypaethralUserData.tracks 135 139 |> hypaethralLenses.setTracks model 136 - |> updateSearchIndex 140 + |> updateSearchIndex value 137 141 |> andThen saveHypaethralData 138 142 139 143 140 - updateSearchIndex : Model -> ( Model, Cmd Msg ) 141 - updateSearchIndex model = 144 + updateSearchIndex : Json.Value -> Model -> ( Model, Cmd Msg ) 145 + updateSearchIndex value model = 142 146 update 143 - (model.hypaethralUserData.tracks 147 + (value 144 148 |> Tracks.UpdateSearchIndex 145 149 |> TracksMsg 146 150 ) ··· 253 257 -- Children 254 258 ----------------------------------------- 255 259 , Sub.map ProcessingMsg (Processing.subscriptions model.processing) 260 + , Sub.map TracksMsg (Tracks.subscriptions model.tracks) 256 261 ] 257 262 258 263
+11 -1
src/Applications/Brain/Ports.elm
··· 1 - port module Brain.Ports exposing (fromCache, fromUI, receiveTags, removeCache, requestCache, requestTags, toCache, toUI) 1 + port module Brain.Ports exposing (fromCache, fromUI, receiveSearchResults, receiveTags, removeCache, requestCache, requestSearch, requestTags, toCache, toUI, updateSearchIndex) 2 2 3 3 import Alien 4 + import Json.Encode as Json 4 5 import Sources.Processing exposing (ContextForTags) 5 6 6 7 ··· 12 13 13 14 14 15 port requestCache : Alien.Event -> Cmd msg 16 + 17 + 18 + port requestSearch : String -> Cmd msg 15 19 16 20 17 21 port requestTags : ContextForTags -> Cmd msg ··· 23 27 port toUI : Alien.Event -> Cmd msg 24 28 25 29 30 + port updateSearchIndex : Json.Value -> Cmd msg 31 + 32 + 26 33 27 34 -- 📰 28 35 ··· 31 38 32 39 33 40 port fromUI : (Alien.Event -> msg) -> Sub msg 41 + 42 + 43 + port receiveSearchResults : (List String -> msg) -> Sub msg 34 44 35 45 36 46 port receiveTags : (ContextForTags -> msg) -> Sub msg
+20 -60
src/Applications/Brain/Tracks.elm
··· 1 - module Brain.Tracks exposing (IndexedTrack, Model, Msg(..), createSearchIndex, initialModel, update) 1 + module Brain.Tracks exposing (Model, Msg(..), initialModel, subscriptions, update) 2 2 3 3 import Alien 4 + import Brain.Ports as Ports 4 5 import Brain.Reply exposing (Reply(..)) 5 - import ElmTextSearch 6 6 import Json.Encode as Json 7 7 import Replying exposing (R3D3) 8 8 import Tracks exposing (Track) ··· 13 13 14 14 15 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 - } 16 + {} 28 17 29 18 30 19 initialModel : Model 31 20 initialModel = 32 - { searchIndex = createSearchIndex [] 33 - } 21 + {} 34 22 35 23 36 24 ··· 39 27 40 28 type Msg 41 29 = Search String 42 - | UpdateSearchIndex (List Track) 30 + | Searched (List String) 31 + | UpdateSearchIndex Json.Value 43 32 44 33 45 34 update : Msg -> Model -> R3D3 Model Msg Reply 46 35 update msg model = 47 36 case msg of 48 37 Search term -> 49 - let 50 - ( updatedIndex, results ) = 51 - Result.withDefault 52 - ( model.searchIndex, [] ) 53 - (ElmTextSearch.search 54 - term 55 - model.searchIndex 56 - ) 38 + ( model 39 + , Ports.requestSearch term 40 + , Nothing 41 + ) 57 42 58 - json = 59 - results 60 - |> List.map Tuple.first 61 - |> Json.list Json.string 62 - in 63 - ( { model | searchIndex = updatedIndex } 43 + Searched results -> 44 + ( model 64 45 , Cmd.none 65 - , Just [ GiveUI Alien.SearchTracks json ] 46 + , Just [ GiveUI Alien.SearchTracks <| Json.list Json.string results ] 66 47 ) 67 48 68 - UpdateSearchIndex tracks -> 69 - ( { model | searchIndex = createSearchIndex tracks } 70 - , Cmd.none 49 + UpdateSearchIndex tracksJson -> 50 + ( model 51 + , Ports.updateSearchIndex tracksJson 71 52 , Nothing 72 53 ) 73 54 74 55 75 56 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 57 + -- 📰 92 58 93 59 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 - } 60 + subscriptions : Model -> Sub Msg 61 + subscriptions _ = 62 + Ports.receiveSearchResults Searched
+6 -2
src/Applications/UI.elm
··· 18 18 import Html.Styled.Lazy as Lazy 19 19 import Json.Decode 20 20 import Json.Encode 21 - import Replying exposing (do, return) 21 + import Replying exposing (andThen, do, return) 22 22 import Return2 as R2 23 23 import Sources 24 24 import Sources.Encoding ··· 74 74 , navKey = key 75 75 , page = Page.fromUrl url 76 76 , url = url 77 + , viewport = flags.viewport 77 78 78 79 -- Children 79 80 ----------- ··· 278 279 |> Result.withDefault Json.Encode.null 279 280 |> LoadHypaethralUserData 280 281 ) 282 + |> andThen (update Core.SaveFavourites) 283 + |> andThen (update Core.SaveSources) 284 + |> andThen (update Core.SaveTracks) 281 285 -- TODO: 282 286 -- Show notication relating to import 283 287 |> R2.addCmd (do <| ChangeUrlUsingPage Page.Index) ··· 479 483 ----------------------------------------- 480 484 , UI.Kit.vessel 481 485 [ model.tracks 482 - |> Lazy.lazy2 UI.Tracks.view model.page 486 + |> Lazy.lazy3 UI.Tracks.view model.page model.viewport.height 483 487 |> Html.map TracksMsg 484 488 485 489 -- Pages
+8 -1
src/Applications/UI/Core.elm
··· 19 19 20 20 21 21 type alias Flags = 22 - {} 22 + { viewport : Viewport } 23 23 24 24 25 25 ··· 32 32 , navKey : Nav.Key 33 33 , page : Page 34 34 , url : Url 35 + , viewport : Viewport 35 36 36 37 ----------------------------------------- 37 38 -- Children ··· 40 41 , backdrop : UI.Backdrop.Model 41 42 , sources : UI.Sources.Model 42 43 , tracks : UI.Tracks.Model 44 + } 45 + 46 + 47 + type alias Viewport = 48 + { height : Float 49 + , width : Float 43 50 } 44 51 45 52
+1
src/Applications/UI/Kit.elm
··· 53 53 -- Other 54 54 , background = rgb 2 7 14 55 55 , focus = rgb 0 0 0 56 + , selection = rgb 3 48 63 56 57 , text = colorKit.base01 57 58 } 58 59
+35 -13
src/Applications/UI/Tracks.elm
··· 10 10 import Html.Styled.Events exposing (onBlur, onClick, onInput) 11 11 import Html.Styled.Ext exposing (onEnterKey) 12 12 import Html.Styled.Lazy exposing (..) 13 + import InfiniteList 13 14 import Json.Decode as Json 14 15 import Json.Encode 15 16 import Material.Icons.Action as Icons ··· 28 29 import UI.Page exposing (Page) 29 30 import UI.Ports 30 31 import UI.Reply exposing (Reply(..)) 32 + import UI.Tracks.Scene.List 31 33 32 34 33 35 ··· 39 41 , enabledSourceIds : List String 40 42 , favourites : List Favourite 41 43 , favouritesOnly : Bool 44 + , infiniteList : InfiniteList.Model 42 45 , nowPlaying : Maybe IdentifiedTrack 46 + , scene : Scene 43 47 , searchResults : Maybe (List String) 44 48 , searchTerm : Maybe String 45 49 , sortBy : SortBy ··· 47 51 } 48 52 49 53 54 + type Scene 55 + = List 56 + 57 + 50 58 initialModel : Model 51 59 initialModel = 52 60 { collection = emptyCollection 53 61 , enabledSourceIds = [] 54 62 , favourites = [] 55 63 , favouritesOnly = False 64 + , infiniteList = InfiniteList.init 56 65 , nowPlaying = Nothing 66 + , scene = List 57 67 , searchResults = Nothing 58 68 , searchTerm = Nothing 59 69 , sortBy = Artist ··· 67 77 68 78 type Msg 69 79 = Bypass 80 + | InfiniteListMsg InfiniteList.Model 70 81 | SetEnabledSourceIds (List String) 71 82 ----------------------------------------- 72 83 -- Collection ··· 93 104 Bypass -> 94 105 Return3.withNothing model 95 106 107 + InfiniteListMsg infiniteList -> 108 + Return3.withNothing { model | infiniteList = infiniteList } 109 + 96 110 SetEnabledSourceIds sourceIds -> 97 111 Return3.withNothing { model | enabledSourceIds = sourceIds } 98 112 ··· 160 174 case ( model.searchTerm, model.searchResults ) of 161 175 ( Just term, _ ) -> 162 176 ( model 163 - , UI.Ports.giveBrain Alien.SearchTracks (Json.Encode.string term) 177 + , UI.Ports.giveBrain Alien.SearchTracks (Json.Encode.string <| String.trim term) 164 178 , Nothing 165 179 ) 166 180 ··· 183 197 "" -> 184 198 { model | searchTerm = Nothing } 185 199 186 - t -> 187 - { model | searchTerm = Just t } 200 + _ -> 201 + { model | searchTerm = Just term } 188 202 ) 189 203 190 204 ··· 234 248 -- 🗺 235 249 236 250 237 - view : Page -> Model -> Html Msg 238 - view page model = 251 + view : Page -> Float -> Model -> Html Msg 252 + view page screenHeight model = 239 253 chunk 240 - [] 254 + [ T.flex 255 + , T.flex_column 256 + , T.flex_grow_1 257 + ] 241 258 [ lazy3 242 259 navigation 243 260 model.favouritesOnly ··· 245 262 page 246 263 247 264 -- 248 - , chunk 249 - [] 250 - (List.map 251 - (\( _, t ) -> text <| t.tags.artist ++ " - " ++ t.tags.title) 252 - model.collection.harvested 253 - ) 265 + , case model.scene of 266 + List -> 267 + UI.Tracks.Scene.List.view 268 + { favouritesOnly = model.favouritesOnly 269 + , infiniteList = model.infiniteList 270 + , screenHeight = screenHeight 271 + , scrollMsg = InfiniteListMsg 272 + } 273 + model.collection.harvested 254 274 ] 255 275 256 276 ··· 391 411 392 412 searchActionsStyles : List Css.Style 393 413 searchActionsStyles = 394 - [ Css.marginTop (Css.px 1) 414 + [ Css.fontSize (Css.px 0) 415 + , Css.lineHeight (Css.px 0) 416 + , Css.marginTop (Css.px 1) 395 417 , Css.paddingRight (Css.px <| 13 - 6) 396 418 , Css.top (Css.pct 50) 397 419 , Css.transform (Css.translateY <| Css.pct -50)
+203
src/Applications/UI/Tracks/Scene/List.elm
··· 1 + module UI.Tracks.Scene.List exposing (view) 2 + 3 + import Chunky exposing (..) 4 + import Color.Ext as Color 5 + import Conditional exposing (ifThenElse) 6 + import Css 7 + import Html as UnstyledHtml 8 + import Html.Attributes as UnstyledHtmlAttributes 9 + import Html.Styled as Html exposing (Html, text) 10 + import Html.Styled.Attributes exposing (css, fromUnstyled) 11 + import InfiniteList 12 + import Tachyons.Classes as T 13 + import Tracks exposing (..) 14 + import UI.Kit 15 + 16 + 17 + 18 + -- 🗺 19 + 20 + 21 + type alias Necessities msg = 22 + { favouritesOnly : Bool 23 + , infiniteList : InfiniteList.Model 24 + , screenHeight : Float 25 + , scrollMsg : InfiniteList.Model -> msg 26 + } 27 + 28 + 29 + view : Necessities msg -> List IdentifiedTrack -> Html msg 30 + view necessities tracks = 31 + let 32 + { favouritesOnly, infiniteList, scrollMsg } = 33 + necessities 34 + in 35 + brick 36 + [ fromUnstyled (InfiniteList.onScroll scrollMsg) ] 37 + [ T.flex_grow_1 38 + , T.vh_25 39 + , T.overflow_x_hidden 40 + , T.overflow_y_scroll 41 + ] 42 + [ Html.fromUnstyled 43 + (InfiniteList.view 44 + (infiniteListConfig necessities) 45 + infiniteList 46 + tracks 47 + ) 48 + ] 49 + 50 + 51 + 52 + -- ROWS 53 + 54 + 55 + rowHeight : Float 56 + rowHeight = 57 + 35 58 + 59 + 60 + rowStyles : Int -> Identifiers -> List Css.Style 61 + rowStyles idx { isNowPlaying } = 62 + let 63 + bgColor = 64 + if isNowPlaying then 65 + Color.toElmCssColor UI.Kit.colorKit.base0D 66 + 67 + else if modBy 2 idx == 1 then 68 + Css.rgb 252 252 252 69 + 70 + else 71 + Css.rgb 255 255 255 72 + in 73 + [ Css.backgroundColor bgColor 74 + ] 75 + 76 + 77 + 78 + -- COLUMNS 79 + 80 + 81 + favouriteColumn : Bool -> Identifiers -> Html msg 82 + favouriteColumn favouritesOnly identifiers = 83 + brick 84 + [ css (favouriteColumnStyles favouritesOnly identifiers) ] 85 + [ T.dtc, T.pl3, T.v_mid ] 86 + [ if identifiers.isFavourite then 87 + text "t" 88 + 89 + else 90 + text "f" 91 + ] 92 + 93 + 94 + favouriteColumnStyles : Bool -> Identifiers -> List Css.Style 95 + favouriteColumnStyles favouritesOnly { isFavourite, isNowPlaying, isSelected } = 96 + let 97 + color = 98 + if isSelected then 99 + Color.toElmCssColor UI.Kit.colors.selection 100 + 101 + else if isNowPlaying && isFavourite then 102 + Css.rgb 255 255 255 103 + 104 + else if isNowPlaying then 105 + Css.rgba 255 255 255 0.4 106 + 107 + else if favouritesOnly || not isFavourite then 108 + Css.rgb 222 222 222 109 + 110 + else 111 + Color.toElmCssColor UI.Kit.colorKit.base08 112 + in 113 + [ Css.color color 114 + , Css.fontFamilies [ "or-favourites" ] 115 + , Css.height (Css.px rowHeight) 116 + , Css.width (Css.pct 4.5) 117 + ] 118 + 119 + 120 + otherColumn : Float -> Bool -> String -> Html msg 121 + otherColumn width isLast text_ = 122 + brick 123 + [ css (otherColumnStyles width) ] 124 + [ T.dtc 125 + , T.pl2 126 + , T.truncate 127 + , T.v_mid 128 + 129 + -- 130 + , ifThenElse isLast T.pr3 T.pr2 131 + ] 132 + [ text text_ ] 133 + 134 + 135 + otherColumnStyles : Float -> List Css.Style 136 + otherColumnStyles columnWidth = 137 + [ Css.height (Css.px rowHeight) 138 + , Css.width (Css.pct columnWidth) 139 + ] 140 + 141 + 142 + 143 + -- INFINITE LIST 144 + 145 + 146 + infiniteListConfig : Necessities msg -> InfiniteList.Config IdentifiedTrack msg 147 + infiniteListConfig necessities = 148 + InfiniteList.withCustomContainer 149 + infiniteListContainer 150 + (InfiniteList.config 151 + { itemView = itemView necessities 152 + , itemHeight = InfiniteList.withConstantHeight (round rowHeight) 153 + , containerHeight = round necessities.screenHeight 154 + } 155 + ) 156 + 157 + 158 + infiniteListContainer : 159 + List ( String, String ) 160 + -> List (UnstyledHtml.Html msg) 161 + -> UnstyledHtml.Html msg 162 + infiniteListContainer styles children = 163 + UnstyledHtml.div 164 + (List.map (\( k, v ) -> UnstyledHtmlAttributes.style k v) styles) 165 + [ (Html.toUnstyled << rawy) <| 166 + slab 167 + Html.ol 168 + [ css listStyles ] 169 + [ T.dt 170 + , T.dt__fixed 171 + , T.f6 172 + , T.list 173 + , T.ma0 174 + , T.ph0 175 + , T.pv1 176 + ] 177 + (List.map Html.fromUnstyled children) 178 + ] 179 + 180 + 181 + listStyles : List Css.Style 182 + listStyles = 183 + [ Css.fontSize (Css.px 12.5) 184 + ] 185 + 186 + 187 + itemView : Necessities msg -> Int -> Int -> IdentifiedTrack -> UnstyledHtml.Html msg 188 + itemView { favouritesOnly } _ idx ( identifiers, track ) = 189 + Html.toUnstyled <| 190 + slab 191 + Html.li 192 + [ css (rowStyles idx identifiers) ] 193 + [ T.dt_row 194 + 195 + -- 196 + , ifThenElse identifiers.isMissing "" T.pointer 197 + , ifThenElse identifiers.isSelected T.fw6 "" 198 + ] 199 + [ favouriteColumn favouritesOnly identifiers 200 + , otherColumn 37.5 False track.tags.title 201 + , otherColumn 29.0 False track.tags.artist 202 + , otherColumn 29.0 True track.tags.album 203 + ]
+37 -3
src/Javascript/Workers/brain.js
··· 33 33 // ----- 34 34 35 35 app.ports.removeCache.subscribe(event => { 36 - deleteFromIndex({ key: event.tag }) 36 + deleteFromIndex({ key: event.tag }).catch(console.error) 37 37 }) 38 38 39 39 ··· 44 44 data: data, 45 45 error: null 46 46 }) 47 - }) 47 + }).catch( 48 + console.error 49 + ) 48 50 }) 49 51 50 52 51 53 app.ports.toCache.subscribe(event => { 52 - setInIndex({ key: event.tag, data: event.data }) 54 + setInIndex({ key: event.tag, data: event.data }).catch(console.error) 53 55 }) 56 + 57 + 58 + 59 + // Search 60 + // ------ 61 + 62 + const search = new Worker("/workers/search.js") 63 + 64 + 65 + app.ports.requestSearch.subscribe(searchTerm => { 66 + search.postMessage({ 67 + action: "PERFORM_SEARCH", 68 + data: searchTerm 69 + }) 70 + }) 71 + 72 + 73 + app.ports.updateSearchIndex.subscribe(tracksJson => { 74 + search.postMessage({ 75 + action: "UPDATE_SEARCH_INDEX", 76 + data: tracksJson 77 + }) 78 + }) 79 + 80 + 81 + search.onmessage = event => { 82 + switch (event.data.action) { 83 + case "PERFORM_SEARCH": 84 + app.ports.receiveSearchResults.send(event.data.data) 85 + break 86 + } 87 + } 54 88 55 89 56 90
+76
src/Javascript/Workers/search.js
··· 1 + // 2 + // Search worker 3 + // (◡ ‿ ◡ ✿) 4 + // 5 + // This worker is responsible for searching through a `Track` collection. 6 + 7 + importScripts("/vendor/lunr.js") 8 + 9 + 10 + let index 11 + 12 + 13 + 14 + // Incoming messages 15 + // ----------------- 16 + 17 + self.onmessage = event => { 18 + switch (event.data.action) { 19 + case "PERFORM_SEARCH": 20 + performSearch(event.data.data) 21 + break 22 + 23 + case "UPDATE_SEARCH_INDEX": 24 + updateSearchIndex(event.data.data) 25 + break 26 + } 27 + } 28 + 29 + 30 + 31 + // Mapper 32 + // ------ 33 + 34 + const mapTrack = track => ({ 35 + id: track.id, 36 + album: track.tags.album, 37 + artist: track.tags.artist, 38 + title: track.tags.title 39 + }) 40 + 41 + 42 + 43 + // Actions 44 + // ------- 45 + 46 + function performSearch(searchTerm) { 47 + let results = [] 48 + 49 + if (index) { 50 + results = index 51 + .search(searchTerm.replace(" *", "")) 52 + .map(s => s.ref) 53 + } 54 + 55 + self.postMessage({ 56 + action: "PERFORM_SEARCH", 57 + data: results 58 + }) 59 + } 60 + 61 + 62 + function updateSearchIndex(input) { 63 + const tracks = (typeof input == "string") 64 + ? JSON.parse(input) 65 + : input 66 + 67 + index = lunr(function() { 68 + this.field("album", {}); 69 + this.field("artist", {}); 70 + this.field("title", {}); 71 + 72 + (tracks || []) 73 + .map(mapTrack) 74 + .forEach(t => this.add(t)) 75 + }) 76 + }
+6 -1
src/Javascript/index.js
··· 8 8 9 9 const app = Elm.UI.init({ 10 10 node: document.getElementById("elm"), 11 - flags: {} 11 + flags: { 12 + viewport: { 13 + height: window.innerHeight, 14 + width: window.innerWidth 15 + } 16 + } 12 17 }) 13 18 14 19
+1
system/Vendor/Main.hs
··· 14 14 main = 15 15 Shikensu.listRelative 16 16 [ "node_modules/fast-text-encoding/text.min.js" 17 + , "node_modules/lunr/lunr.js" 17 18 , "node_modules/tachyons/css/tachyons.min.css" 18 19 , "vendor/music-metadata.min.js" 19 20 ]