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.

Switch to hash-based routing to simplify hosting

+148 -109
+1 -1
Makefile
··· 96 96 97 97 server: 98 98 @echo "> Booting up web server on port 5000" 99 - @devd --port 5000 --all --crossdomain --quiet --notfound=index.html $(BUILD_DIR) 99 + @devd --port 5000 --all --crossdomain --quiet --notfound=301.html $(BUILD_DIR) 100 100 101 101 102 102 test:
+7 -15
README.md
··· 42 42 43 43 ### Hosting on your own server 44 44 45 - Diffuse is a static web application, which means it's just HTML, CSS and Javascript. No REST API, database, or anything backend-related involved. That said, the app does require a HTTP web server so it can have "clean urls" and use service workers (preferably HTTPS). It also requires one special rule, and that is, no matter which HTML page is requested, it should always render the root `200.html` or `index.html` file. `https://diffuse.sh` uses Netlify, which in turn uses the `_redirects` file for this. You can download a pre-build web-only version of Diffuse on the [releases](https://github.com/icidasset/diffuse/releases) page. 45 + Diffuse is a static web application, which means it's just HTML, CSS and Javascript. No REST API, database, or anything backend-related involved. The app uses a hash, aka. fragment, based routing system, so you don't need any special server rules for routing. You can download a pre-build web-only version of Diffuse on the [releases](https://github.com/icidasset/diffuse/releases) page. Diffuse uses service workers, so you may need HTTPS for it to work smoothly in certain browsers. I should also note that some source services use OAuth, so you'll need to use your own application credentials (eg. google drive client id + secret). 46 46 47 47 In short: 48 48 - Diffuse is a static, serverless, web application 49 - - Diffuse requires a HTTP server (prefer HTTPS for service worker) 50 - - Always render the root `200.html` or `index.html` file 49 + - Routing is done using hashes/fragments (eg. `diffuse.sh/#/sources`) 51 50 - Download a web build on the [releases](https://github.com/icidasset/diffuse/releases) page 52 - 53 - ```shell 54 - # Example of a nginx configuration 55 - # Disclaimer: I'm not confident this'll actually work, 56 - # but it should be something along these lines. 57 - location ~ .html$ { 58 - try_files $uri /200.html; 59 - } 60 - ``` 51 + - Uses service workers (use HTTPS if possible) 52 + - May need own OAuth application credentials for some source services 61 53 62 54 63 55 ··· 67 59 68 60 ### Building it yourself 69 61 70 - For version numbers, 71 - see `.tool-versions` and `stack.yaml`. 62 + For version numbers, see `.tool-versions` and `stack.yaml`. 63 + All of these, except the last one, can be install using [homebrew](https://brew.sh/). 72 64 73 65 - [Elm](https://elm-lang.org/) programming language 74 66 - [Haskell](https://docs.haskellstack.org/en/stable/README/) programming language 75 67 - [Google Closure Compiler](https://github.com/google/closure-compiler#getting-started) minifying assets 76 - - [Elm Proofread](https://github.com/icidasset/elm-proofread) documentation tests (optional) 77 68 - [Devd](https://github.com/cortesi/devd) web server for development (optional) 78 69 - [Watchexec](https://github.com/watchexec/watchexec) watching for file changes (optional) 70 + - [Elm Proofread](https://github.com/icidasset/elm-proofread) documentation tests (optional) 79 71 80 72 81 73 ```shell
+33 -14
src/Applications/UI.elm
··· 154 154 |> update 155 155 (PageChanged page) 156 156 |> addCommand 157 - (case maybePage of 158 - Just _ -> 159 - Cmd.none 157 + (if Maybe.isNothing maybePage then 158 + resetUrl key url page 160 159 161 - Nothing -> 162 - Nav.replaceUrl key "/" 160 + else 161 + Cmd.none 163 162 ) 164 163 165 164 ··· 383 382 |> update (BackdropMsg Backdrop.Default) 384 383 |> addCommand (Ports.toBrain <| Alien.trigger Alien.SignOut) 385 384 |> addCommand (Ports.activeQueueItemChanged Nothing) 386 - |> addCommand (Nav.pushUrl model.navKey "/") 385 + |> addCommand (Nav.pushUrl model.navKey "") 387 386 388 387 ----------------------------------------- 389 388 -- Children ··· 650 649 |> Nav.pushUrl model.navKey 651 650 |> returnWithModel model 652 651 653 - LinkClicked (Browser.Internal url) -> 654 - if url.path == "/about" then 655 - returnWithModel model (Nav.load "/about") 652 + LinkClicked (Browser.Internal urlWithFragment) -> 653 + let 654 + url = 655 + if urlWithFragment.fragment == Just "/" then 656 + { urlWithFragment | fragment = Nothing } 657 + 658 + else 659 + urlWithFragment 660 + in 661 + if url.path == "about" then 662 + returnWithModel model (Nav.load "about") 656 663 657 664 else 658 665 returnWithModel model (Nav.pushUrl model.navKey <| Url.toString url) ··· 660 667 LinkClicked (Browser.External href) -> 661 668 returnWithModel model (Nav.load href) 662 669 663 - UrlChanged url -> 664 - case Page.fromUrl url of 665 - Just page -> 670 + UrlChanged ({ fragment, query } as urlWithQuery) -> 671 + let 672 + url = 673 + { urlWithQuery | query = Nothing } 674 + in 675 + case ( query, Page.fromUrl url ) of 676 + ( Nothing, Just page ) -> 666 677 { model | page = page, url = url } 667 678 |> return 668 679 |> andThen (update <| PageChanged page) 669 680 670 - Nothing -> 671 - returnWithModel model (Nav.replaceUrl model.navKey "/") 681 + ( Just _, Just page ) -> 682 + returnWithModel model (resetUrl model.navKey url page) 683 + 684 + _ -> 685 + returnWithModel model (resetUrl model.navKey url Page.Index) 672 686 673 687 674 688 updateWithModel : Model -> Msg -> ( Model, Cmd Msg ) 675 689 updateWithModel model msg = 676 690 update msg model 691 + 692 + 693 + resetUrl : Nav.Key -> Url -> Page.Page -> Cmd Msg 694 + resetUrl key url page = 695 + Nav.replaceUrl key (url.path ++ Page.toString page) 677 696 678 697 679 698
+2 -2
src/Applications/UI/Authentication.elm
··· 470 470 [ T.pv3, T.relative ] 471 471 [ img 472 472 [ onClick Cancel 473 - , src "/images/diffuse-light.svg" 473 + , src "images/diffuse-light.svg" 474 474 , width 190 475 475 476 476 -- ··· 582 582 ] 583 583 [ slab 584 584 a 585 - [ href "/about" ] 585 + [ href "about" ] 586 586 [ T.bb 587 587 , T.no_underline 588 588 , T.white_60
+2 -2
src/Applications/UI/Backdrop.elm
··· 181 181 Html.img 182 182 [ css chosenStyles 183 183 , on "load" loadingDecoder 184 - , src ("/images/Background/" ++ c) 184 + , src ("images/Background/" ++ c) 185 185 ] 186 186 [ T.fixed 187 187 , T.overflow_hidden ··· 264 264 style "opacity" "0" 265 265 266 266 -- 267 - , style "background-image" ("url(/images/Background/" ++ loadedBackdrop ++ ")") 267 + , style "background-image" ("url(images/Background/" ++ loadedBackdrop ++ ")") 268 268 , style "background-size" "cover" 269 269 , style "bottom" "-1px" 270 270 , style "left" "-1px"
+1 -1
src/Applications/UI/Kit.elm
··· 586 586 587 587 logoBackdropStyles : List Css.Style 588 588 logoBackdropStyles = 589 - [ Css.backgroundImage (url "/images/diffuse__icon-dark.svg") 589 + [ Css.backgroundImage (url "images/diffuse__icon-dark.svg") 590 590 , Css.backgroundPosition2 (pct -43.5) (px 98) 591 591 , Css.backgroundRepeat Css.noRepeat 592 592 , Css.backgroundSize Css.cover
+43 -19
src/Applications/UI/Page.elm
··· 1 1 module UI.Page exposing (Page(..), fromUrl, sameBase, sources, toString) 2 2 3 + import Maybe.Extra as Maybe 3 4 import Sources exposing (Service(..)) 4 5 import UI.Playlists.Page as Playlists 5 6 import UI.Queue.Page as Queue ··· 29 30 30 31 fromUrl : Url -> Maybe Page 31 32 fromUrl url = 32 - -- For some oauth stuff, replace the query with the fragment 33 - if Maybe.map (String.contains "token=") url.fragment == Just True then 34 - parse route { url | query = url.fragment } 33 + if Maybe.unwrap False (String.contains "path=") url.query == True then 34 + -- Sometimes we have to use this kind of routing when doing redirections 35 + let 36 + maybePath = 37 + url 38 + |> Url.Parser.parse (query (Query.string "path")) 39 + |> Maybe.join 40 + 41 + path = 42 + Maybe.withDefault "" maybePath 43 + in 44 + if Maybe.unwrap False (String.contains "token=") url.fragment == True then 45 + -- For some oauth stuff, replace the query with the fragment 46 + parse route { url | path = path, query = url.fragment } 47 + 48 + else 49 + parse route { url | path = path } 35 50 36 51 else 37 - parse route url 52 + -- Otherwise do hash-based routing and replace the path with the fragment 53 + parse route { url | path = Maybe.withDefault "" url.fragment } 38 54 39 55 40 56 toString : Page -> String 41 - toString page = 57 + toString = 58 + toString_ >> (++) "#/" 59 + 60 + 61 + toString_ : Page -> String 62 + toString_ page = 42 63 case page of 43 64 Equalizer -> 44 - "/equalizer" 65 + "equalizer" 45 66 46 67 Index -> 47 - "/" 68 + "" 48 69 49 70 ----------------------------------------- 50 71 -- Playlists 51 72 ----------------------------------------- 52 73 Playlists Playlists.Index -> 53 - "/playlists" 74 + "playlists" 54 75 55 76 Playlists Playlists.New -> 56 - "/playlists/new" 77 + "playlists/new" 57 78 58 79 Playlists (Playlists.Edit playlistName) -> 59 - "/playlists/edit/" ++ playlistName 80 + "playlists/edit/" ++ playlistName 60 81 61 82 ----------------------------------------- 62 83 -- Queue 63 84 ----------------------------------------- 64 85 Queue Queue.History -> 65 - "/queue/history" 86 + "queue/history" 66 87 67 88 Queue Queue.Index -> 68 - "/queue" 89 + "queue" 69 90 70 91 ----------------------------------------- 71 92 -- Settings 72 93 ----------------------------------------- 73 94 Settings Settings.ImportExport -> 74 - "/settings/import-export" 95 + "settings/import-export" 75 96 76 97 Settings Settings.Index -> 77 - "/settings" 98 + "settings" 78 99 79 100 ----------------------------------------- 80 101 -- Sources 81 102 ----------------------------------------- 82 103 Sources (Sources.Edit sourceId) -> 83 - "/sources/edit/" ++ sourceId 104 + "sources/edit/" ++ sourceId 84 105 85 106 Sources Sources.Index -> 86 - "/sources" 107 + "sources" 87 108 88 109 Sources Sources.New -> 89 - "/sources/new" 110 + "sources/new" 111 + 112 + Sources (Sources.NewThroughRedirect Dropbox _) -> 113 + "sources/new/dropbox" 90 114 91 115 Sources (Sources.NewThroughRedirect Google _) -> 92 - "/sources/new/google" 116 + "sources/new/google" 93 117 94 118 Sources (Sources.NewThroughRedirect _ _) -> 95 - "/sources/new" 119 + "sources/new" 96 120 97 121 98 122 {-| Are the bases of these two pages the same?
+1 -1
src/Applications/UI/Settings.elm
··· 253 253 254 254 backgroundThumbnailInnerStyles : String -> List Css.Style 255 255 backgroundThumbnailInnerStyles filename = 256 - [ Css.backgroundImage (Css.url <| "/images/Background/Thumbnails/" ++ filename) 256 + [ Css.backgroundImage (Css.url <| "images/Background/Thumbnails/" ++ filename) 257 257 , Css.backgroundSize Css.cover 258 258 ]
+2 -2
src/Applications/UI/Sources/Form.elm
··· 338 338 , text ", do. You can find the configuration for that server " 339 339 , UI.Kit.link 340 340 { label = "here" 341 - , url = "/about#CORS__WebDAV" 341 + , url = "about#CORS__WebDAV" 342 342 } 343 343 , text "." 344 344 ] ··· 469 469 , chunk 470 470 [ T.f6, T.lh_title, T.mb4, T.mt1, T.o_50 ] 471 471 [ text "You can find the instructions over " 472 - , UI.Kit.link { label = "here", url = "/about#" ++ id } 472 + , UI.Kit.link { label = "here", url = "about#" ++ id } 473 473 ] 474 474 ] 475 475
+1 -1
src/Applications/UI/Tracks.elm
··· 712 712 [ inline 713 713 [ T.dib, T.mb2 ] 714 714 [ UI.Kit.buttonLink 715 - "/sources/new" 715 + "sources/new" 716 716 UI.Kit.Normal 717 717 (inline 718 718 []
+7 -7
src/Javascript/Workers/brain.js
··· 4 4 // 5 5 // This worker is responsible for everything non-UI. 6 6 7 - importScripts("/vendor/musicmetadata.min.js") 8 - importScripts("/vendor/subworkers-polyfill.min.js") 7 + importScripts("../vendor/musicmetadata.min.js") 8 + importScripts("../vendor/subworkers-polyfill.min.js") 9 9 10 - importScripts("/brain.js") 11 - importScripts("/encryption.js") 12 - importScripts("/indexed-db.js") 13 - importScripts("/processing.js") 14 - importScripts("/urls.js") 10 + importScripts("../brain.js") 11 + importScripts("../encryption.js") 12 + importScripts("../indexed-db.js") 13 + importScripts("../processing.js") 14 + importScripts("../urls.js") 15 15 16 16 17 17 const app = Elm.Brain.init()
+1 -1
src/Javascript/Workers/search.js
··· 4 4 // 5 5 // This worker is responsible for searching through a `Track` collection. 6 6 7 - importScripts("/vendor/lunr.min.js") 7 + importScripts("../vendor/lunr.min.js") 8 8 9 9 10 10 let index
+1 -1
src/Javascript/Workers/service.js
··· 5 5 // This worker is responsible for caching the application 6 6 // so it can be used offline. 7 7 8 - importScripts("/version.js") 8 + importScripts("version.js") 9 9 10 10 11 11 const KEY =
+1 -1
src/Javascript/index.js
··· 26 26 // Brain 27 27 // ===== 28 28 29 - const brain = new Worker("/workers/brain.js") 29 + const brain = new Worker("workers/brain.js") 30 30 31 31 app.ports.toBrain.subscribe(thing => { 32 32 brain.postMessage(thing)
+1 -1
src/Javascript/indexed-db.js
··· 5 5 // The local database. 6 6 // This is used instead of localStorage. 7 7 8 - importScripts("/vendor/text-encoding-polyfill.min.js") 8 + importScripts("../vendor/text-encoding-polyfill.min.js") 9 9 10 10 11 11 const indexedDB =
+1 -1
src/Library/Sources/Services/Dropbox.elm
··· 85 85 in 86 86 [ ( "response_type", "token" ) 87 87 , ( "client_id", Dict.fetch "appKey" "unknown" sourceData ) 88 - , ( "redirect_uri", origin ++ "/sources/new/dropbox" ) 88 + , ( "redirect_uri", origin ++ "?path=sources/new/dropbox" ) 89 89 , ( "state", state ) 90 90 ] 91 91 |> Common.queryString
+2 -2
src/Library/Sources/Services/Google.elm
··· 101 101 [ ( "access_type", "offline" ) 102 102 , ( "client_id", Dict.fetch "clientId" "unknown" sourceData ) 103 103 , ( "prompt", "consent" ) 104 - , ( "redirect_uri", origin ++ "/sources/new/google" ) 104 + , ( "redirect_uri", origin ++ "?path=sources/new/google" ) 105 105 , ( "response_type", "code" ) 106 106 , ( "scope", "https://www.googleapis.com/auth/drive.readonly" ) 107 107 , ( "state", state ) ··· 145 145 , ( "client_secret", Dict.fetch "clientSecret" "" srcData ) 146 146 , ( "code", Dict.fetch "authCode" "" srcData ) 147 147 , ( "grant_type", "authorization_code" ) 148 - , ( "redirect_uri", origin ++ "/sources/new/google" ) 148 + , ( "redirect_uri", origin ++ "?path=sources/new/google" ) 149 149 ] 150 150 151 151 -- Refresh access token
+1 -1
src/Static/Css/Application.css
··· 1 1 body { 2 2 background-color: rgb(2, 7, 14); 3 - background-image: url(/images/ep_naturalblack_pattern.jpg); 3 + background-image: url(images/ep_naturalblack_pattern.jpg); 4 4 } 5 5 6 6
+1 -1
src/Static/Hosting/_redirects
··· 1 - /* /index.html 200 1 + /* /index.html 301
+2
src/Static/Html/301.html
··· 1 + <!DOCTYPE html> 2 + <html><meta http-equiv="refresh" content="0; url=/" /></html>
+36 -32
src/Static/Html/Application.html
··· 9 9 <meta name="media-controllable" /> 10 10 <meta name="apple-mobile-web-app-capable" content="yes" /> 11 11 12 + <!-- <base href="{{ getenv "BASE" "" }}" /> --> 13 + 12 14 <title>Diffuse</title> 13 15 14 16 <!-- Viewport --> 15 17 <meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9, user-scalable=no" /> 16 18 17 19 <!-- Favicons & Mobile --> 18 - <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> 19 - <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> 20 - <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> 21 - <link rel="manifest" href="/site.webmanifest" /> 22 - <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#1E191A" /> 20 + <link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" /> 21 + <link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png" /> 22 + <link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" /> 23 + <link rel="manifest" href="site.webmanifest" /> 24 + <link rel="mask-icon" href="safari-pinned-tab.svg" color="#1E191A" /> 23 25 <meta name="msapplication-TileColor" content="#1E191A" /> 24 26 <meta name="theme-color" content="#1E191A" /> 25 27 26 28 <!-- Stylesheets --> 27 - <link rel="stylesheet" href="/vendor/tachyons.min.css"> 29 + <link rel="stylesheet" href="vendor/tachyons.min.css"> 28 30 29 - <link rel="stylesheet" href="/application.css"> 30 - <link rel="stylesheet" href="/fonts.css"> 31 + <link rel="stylesheet" href="application.css"> 32 + <link rel="stylesheet" href="fonts.css"> 31 33 32 34 <!-- Preload some assets --> 33 - <link rel="preload" href="/fonts/montserrat/bold.woff2" as="font" crossorigin="anonymous" /> 34 - <link rel="preload" href="/fonts/montserrat/extrabold.woff2" as="font" crossorigin="anonymous" /> 35 - <link rel="preload" href="/fonts/montserrat/light.woff2" as="font" crossorigin="anonymous" /> 36 - <link rel="preload" href="/fonts/montserrat/medium.woff2" as="font" crossorigin="anonymous" /> 37 - <link rel="preload" href="/fonts/montserrat/regular.woff2" as="font" crossorigin="anonymous" /> 38 - <link rel="preload" href="/fonts/montserrat/semibold.woff2" as="font" crossorigin="anonymous" /> 39 - <link rel="preload" href="/fonts/sourcesanspro/bold-italic.woff2" as="font" crossorigin="anonymous" /> 40 - <link rel="preload" href="/fonts/sourcesanspro/bold.woff2" as="font" crossorigin="anonymous" /> 41 - <link rel="preload" href="/fonts/sourcesanspro/black.woff2" as="font" crossorigin="anonymous" /> 42 - <link rel="preload" href="/fonts/sourcesanspro/italic.woff2" as="font" crossorigin="anonymous" /> 43 - <link rel="preload" href="/fonts/sourcesanspro/light-italic.woff2" as="font" crossorigin="anonymous" /> 44 - <link rel="preload" href="/fonts/sourcesanspro/light.woff2" as="font" crossorigin="anonymous" /> 45 - <link rel="preload" href="/fonts/sourcesanspro/regular.woff2" as="font" crossorigin="anonymous" /> 46 - <link rel="preload" href="/fonts/sourcesanspro/semibold-italic.woff2" as="font" crossorigin="anonymous" /> 47 - <link rel="preload" href="/fonts/sourcesanspro/semibold.woff2" as="font" crossorigin="anonymous" /> 35 + <link rel="preload" href="fonts/montserrat/bold.woff2" as="font" crossorigin="anonymous" /> 36 + <link rel="preload" href="fonts/montserrat/extrabold.woff2" as="font" crossorigin="anonymous" /> 37 + <link rel="preload" href="fonts/montserrat/light.woff2" as="font" crossorigin="anonymous" /> 38 + <link rel="preload" href="fonts/montserrat/medium.woff2" as="font" crossorigin="anonymous" /> 39 + <link rel="preload" href="fonts/montserrat/regular.woff2" as="font" crossorigin="anonymous" /> 40 + <link rel="preload" href="fonts/montserrat/semibold.woff2" as="font" crossorigin="anonymous" /> 41 + <link rel="preload" href="fonts/sourcesanspro/bold-italic.woff2" as="font" crossorigin="anonymous" /> 42 + <link rel="preload" href="fonts/sourcesanspro/bold.woff2" as="font" crossorigin="anonymous" /> 43 + <link rel="preload" href="fonts/sourcesanspro/black.woff2" as="font" crossorigin="anonymous" /> 44 + <link rel="preload" href="fonts/sourcesanspro/italic.woff2" as="font" crossorigin="anonymous" /> 45 + <link rel="preload" href="fonts/sourcesanspro/light-italic.woff2" as="font" crossorigin="anonymous" /> 46 + <link rel="preload" href="fonts/sourcesanspro/light.woff2" as="font" crossorigin="anonymous" /> 47 + <link rel="preload" href="fonts/sourcesanspro/regular.woff2" as="font" crossorigin="anonymous" /> 48 + <link rel="preload" href="fonts/sourcesanspro/semibold-italic.woff2" as="font" crossorigin="anonymous" /> 49 + <link rel="preload" href="fonts/sourcesanspro/semibold.woff2" as="font" crossorigin="anonymous" /> 48 50 49 - <link rel="preload" href="/images/diffuse__icon-dark.svg" as="image" crossorigin="anonymous" /> 51 + <link rel="preload" href="images/diffuse__icon-dark.svg" as="image" crossorigin="anonymous" /> 50 52 51 53 </head> 52 54 <body> ··· 89 91 90 92 91 93 <!-- Scripts --> 92 - <script src="/vendor/pep.min.js"></script> 93 - <script src="/vendor/subworkers-polyfill.min.js"></script> 94 - <script src="/vendor/tocca.min.js"></script> 94 + <script>const BASE = new URL(document.baseURI).pathname</script> 95 95 96 - <script src="/urls.js"></script> 97 - <script src="/audio-engine.js"></script> 98 - <script src="/application.js"></script> 96 + <script src="vendor/pep.min.js"></script> 97 + <script src="vendor/subworkers-polyfill.min.js"></script> 98 + <script src="vendor/tocca.min.js"></script> 99 + 100 + <script src="urls.js"></script> 101 + <script src="audio-engine.js"></script> 102 + <script src="application.js"></script> 99 103 100 104 <!-- Initialize Boot Procedure --> 101 - <script src="/index.js"></script> 105 + <script src="index.js"></script> 102 106 103 107 <!-- Service worker --> 104 108 <script> 105 109 if ("serviceWorker" in navigator) { 106 - navigator.serviceWorker.register("/service-worker.js") 110 + navigator.serviceWorker.register("service-worker.js") 107 111 } 108 112 </script> 109 113
+1 -3
system/Build/Main.hs
··· 84 84 85 85 flow :: Dependencies -> (Sequence, Dictionary) -> Dictionary 86 86 flow _ (Html, dict) = 87 - dict 88 - |> rename "Application.html" "200.html" 89 - |> clone "200.html" "index.html" 87 + rename "Application.html" "index.html" dict 90 88 91 89 92 90 flow _ (Css, dict) = dict |> map lowerCasePath