diff --git a/Makefile b/Makefile --- a/Makefile +++ b/Makefile @@ -96,7 +96,7 @@ server: @echo "> Booting up web server on port 5000" - @devd --port 5000 --all --crossdomain --quiet --notfound=index.html $(BUILD_DIR) + @devd --port 5000 --all --crossdomain --quiet --notfound=301.html $(BUILD_DIR) test: diff --git a/README.md b/README.md --- a/README.md +++ b/README.md @@ -42,22 +42,14 @@ ### Hosting on your own server -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. +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). In short: - Diffuse is a static, serverless, web application -- Diffuse requires a HTTP server (prefer HTTPS for service worker) -- Always render the root `200.html` or `index.html` file +- Routing is done using hashes/fragments (eg. `diffuse.sh/#/sources`) - Download a web build on the [releases](https://github.com/icidasset/diffuse/releases) page - -```shell -# Example of a nginx configuration -# Disclaimer: I'm not confident this'll actually work, -# but it should be something along these lines. -location ~ .html$ { - try_files $uri /200.html; -} -``` +- Uses service workers (use HTTPS if possible) +- May need own OAuth application credentials for some source services @@ -67,15 +59,15 @@ ### Building it yourself -For version numbers, -see `.tool-versions` and `stack.yaml`. +For version numbers, see `.tool-versions` and `stack.yaml`. +All of these, except the last one, can be install using [homebrew](https://brew.sh/). - [Elm](https://elm-lang.org/) programming language - [Haskell](https://docs.haskellstack.org/en/stable/README/) programming language - [Google Closure Compiler](https://github.com/google/closure-compiler#getting-started) minifying assets -- [Elm Proofread](https://github.com/icidasset/elm-proofread) documentation tests (optional) - [Devd](https://github.com/cortesi/devd) web server for development (optional) - [Watchexec](https://github.com/watchexec/watchexec) watching for file changes (optional) +- [Elm Proofread](https://github.com/icidasset/elm-proofread) documentation tests (optional) ```shell diff --git a/src/Applications/UI.elm b/src/Applications/UI.elm --- a/src/Applications/UI.elm +++ b/src/Applications/UI.elm @@ -154,12 +154,11 @@ |> update (PageChanged page) |> addCommand - (case maybePage of - Just _ -> - Cmd.none + (if Maybe.isNothing maybePage then + resetUrl key url page - Nothing -> - Nav.replaceUrl key "/" + else + Cmd.none ) @@ -383,7 +382,7 @@ |> update (BackdropMsg Backdrop.Default) |> addCommand (Ports.toBrain <| Alien.trigger Alien.SignOut) |> addCommand (Ports.activeQueueItemChanged Nothing) - |> addCommand (Nav.pushUrl model.navKey "/") + |> addCommand (Nav.pushUrl model.navKey "") ----------------------------------------- -- Children @@ -650,9 +649,17 @@ |> Nav.pushUrl model.navKey |> returnWithModel model - LinkClicked (Browser.Internal url) -> - if url.path == "/about" then - returnWithModel model (Nav.load "/about") + LinkClicked (Browser.Internal urlWithFragment) -> + let + url = + if urlWithFragment.fragment == Just "/" then + { urlWithFragment | fragment = Nothing } + + else + urlWithFragment + in + if url.path == "about" then + returnWithModel model (Nav.load "about") else returnWithModel model (Nav.pushUrl model.navKey <| Url.toString url) @@ -660,20 +667,32 @@ LinkClicked (Browser.External href) -> returnWithModel model (Nav.load href) - UrlChanged url -> - case Page.fromUrl url of - Just page -> + UrlChanged ({ fragment, query } as urlWithQuery) -> + let + url = + { urlWithQuery | query = Nothing } + in + case ( query, Page.fromUrl url ) of + ( Nothing, Just page ) -> { model | page = page, url = url } |> return |> andThen (update <| PageChanged page) - Nothing -> - returnWithModel model (Nav.replaceUrl model.navKey "/") + ( Just _, Just page ) -> + returnWithModel model (resetUrl model.navKey url page) + + _ -> + returnWithModel model (resetUrl model.navKey url Page.Index) updateWithModel : Model -> Msg -> ( Model, Cmd Msg ) updateWithModel model msg = update msg model + + +resetUrl : Nav.Key -> Url -> Page.Page -> Cmd Msg +resetUrl key url page = + Nav.replaceUrl key (url.path ++ Page.toString page) diff --git a/src/Javascript/index.js b/src/Javascript/index.js --- a/src/Javascript/index.js +++ b/src/Javascript/index.js @@ -26,7 +26,7 @@ // Brain // ===== -const brain = new Worker("/workers/brain.js") +const brain = new Worker("workers/brain.js") app.ports.toBrain.subscribe(thing => { brain.postMessage(thing) diff --git a/src/Javascript/indexed-db.js b/src/Javascript/indexed-db.js --- a/src/Javascript/indexed-db.js +++ b/src/Javascript/indexed-db.js @@ -5,7 +5,7 @@ // The local database. // This is used instead of localStorage. -importScripts("/vendor/text-encoding-polyfill.min.js") +importScripts("../vendor/text-encoding-polyfill.min.js") const indexedDB = diff --git a/system/Build/Main.hs b/system/Build/Main.hs --- a/system/Build/Main.hs +++ b/system/Build/Main.hs @@ -84,9 +84,7 @@ flow :: Dependencies -> (Sequence, Dictionary) -> Dictionary flow _ (Html, dict) = - dict - |> rename "Application.html" "200.html" - |> clone "200.html" "index.html" + rename "Application.html" "index.html" dict flow _ (Css, dict) = dict |> map lowerCasePath diff --git a/src/Applications/UI/Authentication.elm b/src/Applications/UI/Authentication.elm --- a/src/Applications/UI/Authentication.elm +++ b/src/Applications/UI/Authentication.elm @@ -470,7 +470,7 @@ [ T.pv3, T.relative ] [ img [ onClick Cancel - , src "/images/diffuse-light.svg" + , src "images/diffuse-light.svg" , width 190 -- @@ -582,7 +582,7 @@ ] [ slab a - [ href "/about" ] + [ href "about" ] [ T.bb , T.no_underline , T.white_60 diff --git a/src/Applications/UI/Backdrop.elm b/src/Applications/UI/Backdrop.elm --- a/src/Applications/UI/Backdrop.elm +++ b/src/Applications/UI/Backdrop.elm @@ -181,7 +181,7 @@ Html.img [ css chosenStyles , on "load" loadingDecoder - , src ("/images/Background/" ++ c) + , src ("images/Background/" ++ c) ] [ T.fixed , T.overflow_hidden @@ -264,7 +264,7 @@ style "opacity" "0" -- - , style "background-image" ("url(/images/Background/" ++ loadedBackdrop ++ ")") + , style "background-image" ("url(images/Background/" ++ loadedBackdrop ++ ")") , style "background-size" "cover" , style "bottom" "-1px" , style "left" "-1px" diff --git a/src/Applications/UI/Kit.elm b/src/Applications/UI/Kit.elm --- a/src/Applications/UI/Kit.elm +++ b/src/Applications/UI/Kit.elm @@ -586,7 +586,7 @@ logoBackdropStyles : List Css.Style logoBackdropStyles = - [ Css.backgroundImage (url "/images/diffuse__icon-dark.svg") + [ Css.backgroundImage (url "images/diffuse__icon-dark.svg") , Css.backgroundPosition2 (pct -43.5) (px 98) , Css.backgroundRepeat Css.noRepeat , Css.backgroundSize Css.cover diff --git a/src/Applications/UI/Page.elm b/src/Applications/UI/Page.elm --- a/src/Applications/UI/Page.elm +++ b/src/Applications/UI/Page.elm @@ -1,5 +1,6 @@ module UI.Page exposing (Page(..), fromUrl, sameBase, sources, toString) +import Maybe.Extra as Maybe import Sources exposing (Service(..)) import UI.Playlists.Page as Playlists import UI.Queue.Page as Queue @@ -29,70 +30,93 @@ fromUrl : Url -> Maybe Page fromUrl url = - -- For some oauth stuff, replace the query with the fragment - if Maybe.map (String.contains "token=") url.fragment == Just True then - parse route { url | query = url.fragment } + if Maybe.unwrap False (String.contains "path=") url.query == True then + -- Sometimes we have to use this kind of routing when doing redirections + let + maybePath = + url + |> Url.Parser.parse (query (Query.string "path")) + |> Maybe.join + + path = + Maybe.withDefault "" maybePath + in + if Maybe.unwrap False (String.contains "token=") url.fragment == True then + -- For some oauth stuff, replace the query with the fragment + parse route { url | path = path, query = url.fragment } + + else + parse route { url | path = path } else - parse route url + -- Otherwise do hash-based routing and replace the path with the fragment + parse route { url | path = Maybe.withDefault "" url.fragment } toString : Page -> String -toString page = +toString = + toString_ >> (++) "#/" + + +toString_ : Page -> String +toString_ page = case page of Equalizer -> - "/equalizer" + "equalizer" Index -> - "/" + "" ----------------------------------------- -- Playlists ----------------------------------------- Playlists Playlists.Index -> - "/playlists" + "playlists" Playlists Playlists.New -> - "/playlists/new" + "playlists/new" Playlists (Playlists.Edit playlistName) -> - "/playlists/edit/" ++ playlistName + "playlists/edit/" ++ playlistName ----------------------------------------- -- Queue ----------------------------------------- Queue Queue.History -> - "/queue/history" + "queue/history" Queue Queue.Index -> - "/queue" + "queue" ----------------------------------------- -- Settings ----------------------------------------- Settings Settings.ImportExport -> - "/settings/import-export" + "settings/import-export" Settings Settings.Index -> - "/settings" + "settings" ----------------------------------------- -- Sources ----------------------------------------- Sources (Sources.Edit sourceId) -> - "/sources/edit/" ++ sourceId + "sources/edit/" ++ sourceId Sources Sources.Index -> - "/sources" + "sources" Sources Sources.New -> - "/sources/new" + "sources/new" + + Sources (Sources.NewThroughRedirect Dropbox _) -> + "sources/new/dropbox" Sources (Sources.NewThroughRedirect Google _) -> - "/sources/new/google" + "sources/new/google" Sources (Sources.NewThroughRedirect _ _) -> - "/sources/new" + "sources/new" {-| Are the bases of these two pages the same? diff --git a/src/Applications/UI/Settings.elm b/src/Applications/UI/Settings.elm --- a/src/Applications/UI/Settings.elm +++ b/src/Applications/UI/Settings.elm @@ -253,6 +253,6 @@ backgroundThumbnailInnerStyles : String -> List Css.Style backgroundThumbnailInnerStyles filename = - [ Css.backgroundImage (Css.url <| "/images/Background/Thumbnails/" ++ filename) + [ Css.backgroundImage (Css.url <| "images/Background/Thumbnails/" ++ filename) , Css.backgroundSize Css.cover ] diff --git a/src/Applications/UI/Tracks.elm b/src/Applications/UI/Tracks.elm --- a/src/Applications/UI/Tracks.elm +++ b/src/Applications/UI/Tracks.elm @@ -712,7 +712,7 @@ [ inline [ T.dib, T.mb2 ] [ UI.Kit.buttonLink - "/sources/new" + "sources/new" UI.Kit.Normal (inline [] diff --git a/src/Javascript/Workers/brain.js b/src/Javascript/Workers/brain.js --- a/src/Javascript/Workers/brain.js +++ b/src/Javascript/Workers/brain.js @@ -4,14 +4,14 @@ // // This worker is responsible for everything non-UI. -importScripts("/vendor/musicmetadata.min.js") -importScripts("/vendor/subworkers-polyfill.min.js") +importScripts("../vendor/musicmetadata.min.js") +importScripts("../vendor/subworkers-polyfill.min.js") -importScripts("/brain.js") -importScripts("/encryption.js") -importScripts("/indexed-db.js") -importScripts("/processing.js") -importScripts("/urls.js") +importScripts("../brain.js") +importScripts("../encryption.js") +importScripts("../indexed-db.js") +importScripts("../processing.js") +importScripts("../urls.js") const app = Elm.Brain.init() diff --git a/src/Javascript/Workers/search.js b/src/Javascript/Workers/search.js --- a/src/Javascript/Workers/search.js +++ b/src/Javascript/Workers/search.js @@ -4,7 +4,7 @@ // // This worker is responsible for searching through a `Track` collection. -importScripts("/vendor/lunr.min.js") +importScripts("../vendor/lunr.min.js") let index diff --git a/src/Javascript/Workers/service.js b/src/Javascript/Workers/service.js --- a/src/Javascript/Workers/service.js +++ b/src/Javascript/Workers/service.js @@ -5,7 +5,7 @@ // This worker is responsible for caching the application // so it can be used offline. -importScripts("/version.js") +importScripts("version.js") const KEY = diff --git a/src/Static/Css/Application.css b/src/Static/Css/Application.css --- a/src/Static/Css/Application.css +++ b/src/Static/Css/Application.css @@ -1,6 +1,6 @@ body { background-color: rgb(2, 7, 14); - background-image: url(/images/ep_naturalblack_pattern.jpg); + background-image: url(images/ep_naturalblack_pattern.jpg); } diff --git a/src/Static/Hosting/_redirects b/src/Static/Hosting/_redirects --- a/src/Static/Hosting/_redirects +++ b/src/Static/Hosting/_redirects @@ -1,1 +1,1 @@ -/* /index.html 200 +/* /index.html 301 diff --git a/src/Static/Html/301.html b/src/Static/Html/301.html new file mode 100644 --- /dev/null +++ b/src/Static/Html/301.html @@ -0,0 +1,2 @@ + + diff --git a/src/Static/Html/Application.html b/src/Static/Html/Application.html --- a/src/Static/Html/Application.html +++ b/src/Static/Html/Application.html @@ -9,44 +9,46 @@ + + Diffuse - - - - - + + + + + - + - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + @@ -89,21 +91,23 @@ - - - + - - - + + + + + + + - + diff --git a/src/Applications/UI/Sources/Form.elm b/src/Applications/UI/Sources/Form.elm --- a/src/Applications/UI/Sources/Form.elm +++ b/src/Applications/UI/Sources/Form.elm @@ -338,7 +338,7 @@ , text ", do. You can find the configuration for that server " , UI.Kit.link { label = "here" - , url = "/about#CORS__WebDAV" + , url = "about#CORS__WebDAV" } , text "." ] @@ -469,7 +469,7 @@ , chunk [ T.f6, T.lh_title, T.mb4, T.mt1, T.o_50 ] [ text "You can find the instructions over " - , UI.Kit.link { label = "here", url = "/about#" ++ id } + , UI.Kit.link { label = "here", url = "about#" ++ id } ] ] diff --git a/src/Library/Sources/Services/Dropbox.elm b/src/Library/Sources/Services/Dropbox.elm --- a/src/Library/Sources/Services/Dropbox.elm +++ b/src/Library/Sources/Services/Dropbox.elm @@ -85,7 +85,7 @@ in [ ( "response_type", "token" ) , ( "client_id", Dict.fetch "appKey" "unknown" sourceData ) - , ( "redirect_uri", origin ++ "/sources/new/dropbox" ) + , ( "redirect_uri", origin ++ "?path=sources/new/dropbox" ) , ( "state", state ) ] |> Common.queryString diff --git a/src/Library/Sources/Services/Google.elm b/src/Library/Sources/Services/Google.elm --- a/src/Library/Sources/Services/Google.elm +++ b/src/Library/Sources/Services/Google.elm @@ -101,7 +101,7 @@ [ ( "access_type", "offline" ) , ( "client_id", Dict.fetch "clientId" "unknown" sourceData ) , ( "prompt", "consent" ) - , ( "redirect_uri", origin ++ "/sources/new/google" ) + , ( "redirect_uri", origin ++ "?path=sources/new/google" ) , ( "response_type", "code" ) , ( "scope", "https://www.googleapis.com/auth/drive.readonly" ) , ( "state", state ) @@ -145,7 +145,7 @@ , ( "client_secret", Dict.fetch "clientSecret" "" srcData ) , ( "code", Dict.fetch "authCode" "" srcData ) , ( "grant_type", "authorization_code" ) - , ( "redirect_uri", origin ++ "/sources/new/google" ) + , ( "redirect_uri", origin ++ "?path=sources/new/google" ) ] -- Refresh access token