diff --git a/Cargo.lock b/Cargo.lock index 2c5bbb9..f666c06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,9 +339,9 @@ dependencies = [ [[package]] name = "bberry" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9413ba3ccb3dfc908093b3e2cdb7b39acc2127fe0b29268df4708cfa06c6ce30" +checksum = "d73b8674872fdd2eb09463fc9e3d15c4510a3abd8a1f8ef0a9e918065913aed5" [[package]] name = "bit_field" diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 15e868f..117a1cb 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -48,4 +48,4 @@ async-stripe = { version = "0.41.0", features = [ ] } emojis = "0.6.4" webp = "0.3.0" -bberry = "0.1.1" +bberry = "0.1.2" diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index e5be918..121e3a3 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -36,7 +36,7 @@ pub const ME_JS: &str = include_str!("./public/js/me.js"); pub const STREAMS_JS: &str = include_str!("./public/js/streams.js"); // html -pub const ROOT: &str = include_str!("./public/html/root.html"); +pub const ROOT: &str = include_str!("./public/html/root.lisp"); pub const MACROS: &str = include_str!("./public/html/macros.html"); pub const COMPONENTS: &str = include_str!("./public/html/components.html"); @@ -140,7 +140,13 @@ pub(crate) async fn pull_icon(icon: &str, icons_dir: &str) { } println!("download icon: {icon}"); - let svg = reqwest::get(icon_url).await.unwrap().text().await.unwrap(); + let svg = reqwest::get(icon_url) + .await + .unwrap() + .text() + .await + .unwrap() + .replace("\n", ""); write(&file_path, &svg).unwrap(); writer.insert(icon.to_string(), svg); @@ -245,7 +251,7 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD { // ... let html_path = PathBufD::current().join(&config.dirs.templates); - write_template!(html_path->"root.html"(crate::assets::ROOT) --config=config); + write_template!(html_path->"root.html"(crate::assets::ROOT) --config=config --lisp); write_template!(html_path->"macros.html"(crate::assets::MACROS) --config=config); write_template!(html_path->"components.html"(crate::assets::COMPONENTS) --config=config); diff --git a/crates/app/src/public/html/misc/error.lisp b/crates/app/src/public/html/misc/error.lisp index 1a46af0..e7b4f16 100644 --- a/crates/app/src/public/html/misc/error.lisp +++ b/crates/app/src/public/html/misc/error.lisp @@ -3,24 +3,24 @@ (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main - (: "class" "flex flex-col gap-2") + ("class" "flex flex-col gap-2") (div - (: "class" "card-nest") + ("class" "card-nest") (div - (: "class" "card") + ("class" "card") (b (text "Error 😦"))) (div - (: "class" "card flex flex-col gap-4") + ("class" "card flex flex-col gap-4") (span (text "{{ error_text }}")) (div - (: "class" "w-full flex gap-2") + ("class" "w-full flex gap-2") (a - (: "class" "button primary") (: "href" "/") + ("class" "button primary") ("href" "/") (text "Home")) (a - (: "class" "button secondary") (: "href" "javascript:history.back()") + ("class" "button secondary") ("href" "javascript:history.back()") (text "Back")))))) (text "{% endblock %}") diff --git a/crates/app/src/public/html/misc/markdown.lisp b/crates/app/src/public/html/misc/markdown.lisp index f4312c9..2296fcd 100644 --- a/crates/app/src/public/html/misc/markdown.lisp +++ b/crates/app/src/public/html/misc/markdown.lisp @@ -3,18 +3,18 @@ (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main - (: "class" "flex flex-col gap-2") + ("class" "flex flex-col gap-2") (div - (: "class" "card-nest") + ("class" "card-nest") (div - (: "class" "card small flex items-center justify-between gap-2") + ("class" "card small flex items-center justify-between gap-2") (span - (: "class" "flex items-center gap-2") + ("class" "flex items-center gap-2") (text "{{ icon scroll-text }}") (span (text "{{ file_name }}")))) (div - (: "class" "card") + ("class" "card") (span (text "{{ file|markdown|safe }}"))))) (text "{% endblock %}") diff --git a/crates/app/src/public/html/root.html b/crates/app/src/public/html/root.html deleted file mode 100644 index 5365c67..0000000 --- a/crates/app/src/public/html/root.html +++ /dev/null @@ -1,424 +0,0 @@ -{%- import "components.html" as components -%} {%- import "macros.html" as -macros -%} -<!doctype html> -<html lang="en"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta http-equiv="X-UA-Compatible" content="ie=edge" /> - - <meta - http-equiv="content-security-policy" - content="default-src 'self' blob: *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' blob: *; script-src 'self' 'unsafe-inline' blob: *; object-src 'self' blob: *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' blob: data: *" - /> - - <link rel="icon" href="/public/favicon.svg" /> - - <link rel="stylesheet" href="/css/style.css" /> - - {% if user -%} - <script> - window.localStorage.setItem( - "tetratto:theme", - "{{ user.settings.theme_preference }}", - ); - </script> - {%- endif %} - - <script src="/js/loader.js"></script> - <script defer async src="/js/atto.js"></script> - - <script> - globalThis.ns_verbose = false; - globalThis.ns_config = { - root: "/js/", - verbose: globalThis.ns_verbose, - version: "cache-breaker-{{ random_cache_breaker }}", - }; - - globalThis._app_base = { - name: "tetratto", - ns_store: {}, - classes: {}, - }; - - globalThis.no_policy = false; - </script> - - <meta name="theme-color" content="{{ config.color }}" /> - <meta name="description" content="{{ config.description }}" /> - <meta property="og:type" content="website" /> - <meta property="og:site_name" content="{{ config.name }}" /> - - <meta name="turbo-prefetch" content="false" /> - <meta name="turbo-refresh-method" content="morph" /> - <meta name="turbo-refresh-scroll" content="preserve" /> - - <script - src="https://unpkg.com/@hotwired/turbo@8.0.5/dist/turbo.es2017-esm.js" - type="module" - async - defer - ></script> - - {% block head %}{% endblock %} - </head> - - <body> - <div id="toast_zone"></div> - - <div id="page"> - <!-- prettier-ignore --> - {% if user and user.id == 0 -%} - <article> - <main> - <div class="card-nest"> - <div class="card small flex items-center gap-2 red"> - {{ icon "frown" }} - <span - >{{ text "general:label.account_banned" }}</span - > - </div> - - <div class="card"> - <span - >{{ text "general:label.account_banned_body" - }}</span - > - </div> - </div> - </main> - </article> - {% else %} {% block body %}{% endblock %} {%- endif %} - <!-- html_footer_goes_here --> - </div> - - <script data-turbo-permanent="true" id="init-script"> - document.documentElement.addEventListener("turbo:load", () => { - const atto = ns("atto"); - - atto.disconnect_observers(); - atto.remove_false_options(); - atto.clean_date_codes(); - atto.link_filter(); - - atto["hooks::scroll"](document.body, document.documentElement); - atto["hooks::dropdown.init"](window); - atto["hooks::character_counter.init"](); - atto["hooks::long_text.init"](); - atto["hooks::alt"](); - atto["hooks::online_indicator"](); - atto["hooks::ips"](); - atto["hooks::check_reactions"](); - atto["hooks::tabs"](); - atto["hooks::spotify_time_text"](); // spotify durations - atto["hooks::verify_emoji"](); - - if (document.getElementById("tokens")) { - trigger("me::render_token_picker", [ - document.getElementById("tokens"), - ]); - } - - setTimeout(() => { - trigger("me::notifications_stream"); - }, 250); - }); - </script> - - {% if user -%} - <script data-turbo-permanent="true" id="update-seen-script"> - document.documentElement.addEventListener("turbo:load", () => { - trigger("me::seen"); - trigger("streams::user", ["{{ user.id }}"]); - - if (!window.location.pathname.startsWith("/chats/")) { - if (window.socket) { - window.socket.send("Close"); - window.socket = undefined; - console.log("socket disconnect"); - } - } - }); - </script> - {%- endif %} - - <!-- dialogs --> - <dialog id="link_filter"> - <div class="inner flex flex-col gap-2"> - <p>Pressing continue will bring you to the following URL:</p> - <pre><code id="link_filter_url"></code></pre> - <p>Are sure you want to go there?</p> - - <hr class="margin" /> - <div class="flex gap-2"> - <a - class="button primary" - id="link_filter_continue" - rel="noopener noreferrer" - target="_blank" - onclick="document.getElementById('link_filter').close()" - > - {{ icon "external-link" }} - <span>{{ text "dialog:action.continue" }}</span> - </a> - <button - class="secondary" - type="button" - onclick="document.getElementById('link_filter').close()" - > - {{ icon "x" }} - <span>{{ text "dialog:action.cancel" }}</span> - </button> - </div> - </div> - </dialog> - - <dialog id="web_api_prompt"> - <div class="inner flex flex-col gap-2"> - <form - class="flex gap-2 flex-col" - onsubmit="event.preventDefault()" - > - <label for="prompt" id="web_api_prompt:msg"></label> - <input id="prompt" name="prompt" /> - - <div class="flex justify-between"> - <div></div> - - <div class="flex gap-2"> - <button - class="primary bold circle" - onclick="globalThis.web_api_prompt_submit(document.getElementById('prompt').value); document.getElementById('prompt').value = ''" - type="button" - > - {{ icon "check" }} {{ text "dialog:action.okay" - }} - </button> - - <button - class="bold red camo" - onclick="globalThis.web_api_prompt_submit('')" - type="button" - > - {{ icon "x" }} {{ text "dialog:action.cancel" }} - </button> - </div> - </div> - </form> - </div> - </dialog> - - <dialog id="web_api_prompt_long"> - <div class="inner flex flex-col gap-2"> - <form - class="flex gap-2 flex-col" - onsubmit="event.preventDefault()" - > - <label - for="prompt_long" - id="web_api_prompt_long:msg" - ></label> - <textarea id="prompt_long" name="prompt_long"></textarea> - - <div class="flex justify-between"> - <div></div> - - <div class="flex gap-2"> - <button - class="primary bold circle" - onclick="globalThis.web_api_prompt_long_submit(document.getElementById('prompt_long').value); document.getElementById('prompt_long').value = ''" - type="button" - > - {{ icon "check" }} {{ text "dialog:action.okay" - }} - </button> - - <button - class="bold red camo" - onclick="globalThis.web_api_prompt_long_submit('')" - type="button" - > - {{ icon "x" }} {{ text "dialog:action.cancel" }} - </button> - </div> - </div> - </form> - </div> - </dialog> - - <dialog id="web_api_confirm"> - <div class="inner flex flex-col gap-2"> - <form - class="flex gap-2 flex-col" - onsubmit="event.preventDefault()" - > - <label id="web_api_confirm:msg"></label> - - <div class="flex justify-between"> - <div></div> - - <div class="flex gap-2"> - <button - class="primary bold circle" - onclick="globalThis.web_api_confirm_submit(true)" - type="button" - > - {{ icon "check" }} {{ text "dialog:action.yes" - }} - </button> - - <button - class="bold red camo" - onclick="globalThis.web_api_confirm_submit(false)" - type="button" - > - {{ icon "x" }} {{ text "dialog:action.no" }} - </button> - </div> - </div> - </form> - </div> - </dialog> - - <div class="lightbox hidden" id="lightbox"> - <button - class="lightbox_exit small square quaternary red" - onclick="trigger('ui::lightbox_close')" - > - {{ icon "x" }} - </button> - - <a href="" id="lightbox_img_a" target="_blank"> - <img src="" alt="Image" id="lightbox_img" /> - </a> - </div> - - {% if user -%} - <dialog id="tokens_dialog"> - <div class="inner flex flex-col gap-2"> - <form - class="flex gap-2 flex-col" - onsubmit="event.preventDefault()" - > - <div id="tokens" style="display: contents"></div> - - <a href="/auth/login" class="button" data-turbo="false"> - {{ icon "plus" }} - <span>{{ text "general:action.add_account" }}</span> - </a> - - <div class="flex justify-between"> - <div></div> - - <div class="flex gap-2"> - <button - class="quaternary" - onclick="document.getElementById('tokens_dialog').close()" - type="button" - > - {{ icon "check" }} - <span>{{ text "dialog:action.okay" }}</span> - </button> - </div> - </div> - </form> - </div> - </dialog> - {%- endif %} {% if user and use_user_theme -%} {{ - components::theme(user=user, - theme_preference=user.settings.theme_preference) }} - <script> - setTimeout(() => { - trigger("atto::use_theme_preference"); - }, 150); - </script> - {%- endif %} {% if user and user.connections.Spotify and - config.connections.spotify_client_id and - user.connections.Spotify[0].data.token and - user.connections.Spotify[0].data.refresh_token %} - <script> - setTimeout(async () => { - if (window.spotify_init) { - return; - } - - window.spotify_init = true; - const client_id = "{{ config.connections.spotify_client_id }}"; - let token = "{{ user.connections.Spotify[0].data.token }}"; - let refresh_token = - "{{ user.connections.Spotify[0].data.refresh_token }}"; - - if (token) { - // we already have a token - const pull_playing = async () => { - const playing = await trigger("spotify::get_playing", [ - token, - ]); - - if (playing.error) { - // refresh token - const [new_token, new_refresh_token, expires_in] = - await trigger("spotify::refresh_token", [ - client_id, - refresh_token, - ]); - - await trigger("connections::push_con_data", [ - "Spotify", - { - token: new_token, - expires_in: expires_in.toString(), - name: profile.display_name, - }, - ]); - - token = new_token; - refresh_token = new_refresh_token; - return; - } - - await trigger("spotify::publish_playing", [playing]); - }; - - await pull_playing(); - setInterval(pull_playing, 30_000); - } else { - window.spotify_needs_token = true; - } - }, 150); - </script> - {% elif user and user.connections.LastFm and - config.connections.last_fm_key and - user.connections.LastFm[0].data.session_token %} - <script> - setTimeout(async () => { - if (window.last_fm_init) { - return; - } - - window.last_fm_init = true; - const user = "{{ user.connections.LastFm[0].data.name }}"; - const session_token = - "{{ user.connections.LastFm[0].data.session_token }}"; - - if (session_token) { - // we already have a token - const pull_playing = async () => { - const playing = await trigger("last_fm::get_playing", [ - user, - session_token, - ]); - await trigger("last_fm::publish_playing", [playing]); - }; - - await pull_playing(); - setInterval(pull_playing, 30_000); - } else { - window.last_fm_needs_token = true; - } - }, 150); - </script> - {%- endif %} - </body> -</html> diff --git a/crates/app/src/public/html/root.lisp b/crates/app/src/public/html/root.lisp new file mode 100644 index 0000000..d83d92a --- /dev/null +++ b/crates/app/src/public/html/root.lisp @@ -0,0 +1,381 @@ +(text "{%- import \"components.html\" as components -%} {%- import \"macros.html\" as macros -%}") +(text "<!doctype html>") + +(html + ("lang" "en") + (head + (meta ("charset" "UTF-8")) + (meta ("name" "viewport") ("content" "width=device-width, initial-scale=1.0")) + (meta ("http-equiv" "X-UA-Compatible") ("content" "ie=edge")) + (meta ("http-equiv" "content-security-policy") ("content" "default-src 'self' blob: *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' blob: *; script-src 'self' 'unsafe-inline' blob: *; object-src 'self' blob: *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' blob: data: *")) + + (link ("rel" "icon") ("href" "/public/favicon.svg")) + (link ("rel" "stylesheet") ("href" "/css/style.css")) + + (text "{% if user -%} + <script> + window.localStorage.setItem( + \"tetratto:theme\", + \"{{ user.settings.theme_preference }}\", + ); + </script> + {%- endif %}") + + (text "<script> + globalThis.ns_verbose = false; + globalThis.ns_config = { + root: \"/js/\", + verbose: globalThis.ns_verbose, + version: \"cache-breaker-{{ random_cache_breaker }}\", + }; + + globalThis._app_base = { + name: \"tetratto\", + ns_store: {}, + classes: {}, + }; + + globalThis.no_policy = false; + </script>") + + (script ("src" "/js/loader.js" )) + (script ("src" "/js/atto.js" )) + + (meta ("name" "theme-color") ("content" "{{ config.color }}")) + (meta ("name" "description") ("content" "{{ config.description }}")) + (meta ("property" "og:type") ("content" "website")) + (meta ("property" "og:site_name") ("content" "{{ config.color }}")) + + (meta ("name" "turbo-prefetch") ("content" "false")) + (meta ("name" "turbo-refresh-method") ("content" "morph")) + (meta ("name" "turbo-refresh-scroll") ("content" "preserve")) + + (script ("src" "https://unpkg.com/@hotwired/turbo@8.0.5/dist/turbo.es2017-esm.js") ("type" "module") ("async" "") ("defer" "")) + + (text "{% block head %}{% endblock %}")) + + (body + (div ("id" "toast_zone")) + + (div + ("id" "page") + (text "{% if user and user.id == 0 -%}") + ; account banned message + (article + (main + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2 red") + (text "{{ icon \"frown\" }}") + (span (text "{{ text \"general:label.account_banned\" }}"))) + + (div + ("class" "card") + (span (text "{{ text \"general:label.account_banned_body\" }}")))))) + + ; if we aren't banned, just show the page body + (text "{% else %} {% block body %}{% endblock %} {%- endif %}") + (text "<!-- html_footer_goes_here -->")) + + ; random js + (text "<script data-turbo-permanent=\"true\" id=\"init-script\"> + document.documentElement.addEventListener(\"turbo:load\", () => { + const atto = ns(\"atto\"); + + atto.disconnect_observers(); + atto.remove_false_options(); + atto.clean_date_codes(); + atto.link_filter(); + + atto[\"hooks::scroll\"](document.body, document.documentElement); + atto[\"hooks::dropdown.init\"](window); + atto[\"hooks::character_counter.init\"](); + atto[\"hooks::long_text.init\"](); + atto[\"hooks::alt\"](); + atto[\"hooks::online_indicator\"](); + atto[\"hooks::ips\"](); + atto[\"hooks::check_reactions\"](); + atto[\"hooks::tabs\"](); + atto[\"hooks::spotify_time_text\"](); // spotify durations + atto[\"hooks::verify_emoji\"](); + + if (document.getElementById(\"tokens\")) { + trigger(\"me::render_token_picker\", [ + document.getElementById(\"tokens\"), + ]); + } + + setTimeout(() => { + trigger(\"me::notifications_stream\"); + }, 250); + }); + </script>") + + (text "{% if user -%} + <script data-turbo-permanent=\"true\" id=\"update-seen-script\"> + document.documentElement.addEventListener(\"turbo:load\", () => { + trigger(\"me::seen\"); + trigger(\"streams::user\", [\"{{ user.id }}\"]); + + if (!window.location.pathname.startsWith(\"/chats/\")) { + if (window.socket) { + window.socket.send(\"Close\"); + window.socket = undefined; + console.log(\"socket disconnect\"); + } + } + }); + </script> + {%- endif %}") + + ; dialogs + (dialog + ("id" "link_filter") + (div + ("class" "inner flex flex-col gap-2") + + ; warning stuff + (p (text "Pressing continue will bring you to the following URL:")) + (pre (code ("id" "link_filter_url"))) + (p (text "Are sure you want to go there?")) + + (hr ("class" "margin")) + (div + ("class" "flex gap-2") + + (a + ("class" "button primary") + ("id" "link_filter_continue") + ("rel" "noopener noreferrer") + ("target" "_blank") + ("onclick", "document.getElementById('link_filter').close()") + (text "{{ icon \"external-link\" }}") + (span (text "{{ text \"dialog:action.continue\" }}"))) + + (button + ("class" "secondary") + ("type" "button") + ("onclick", "document.getElementById('link_filter').close()") + (text "{{ icon \"x\" }}" ) + (span (text "{{ text \"dialog:action.cancel\" }}")))))) + + (dialog + ("id" "web_api_prompt") + (div + ("class" "inner flex flex-col gap-2") + (form + ("class" "flex gap-2 flex-col") + ("onsubmit" "event.preventDefault()") + (label ("for" "prompt") ("id" "web_api_prompt:msg")) + (input ("id" "prompt") ("name" "prompt")) + + (div + ("class" "flex justify-between") + (div null?) + (div + ("class" "flex gap-2") + (button + ("class" "primary bold circle") + ("onclick", "globalThis.web_api_prompt_submit(document.getElementById('prompt').value); document.getElementById('prompt').value = ''") + ("type" "button") + (text "{{ icon \"check\" }}") + (text "{{ text \"dialog:action.okay\" }}")) + + (button + ("class" "bold red camo") + ("onclick", "globalThis.web_api_prompt_submit('')") + ("type" "button") + (text "{{ icon \"x\" }}") + (text "{{ text \"dialog:action.cancel\" }}"))))))) + + (dialog + ("id" "web_api_prompt_long") + (div + ("class" "inner flex flex-col gap-2") + (form + ("class" "flex gap-2 flex-col") + ("onsubmit" "event.preventDefault()") + (label ("for" "prompt_long") ("id" "web_api_prompt_long:msg")) + (input ("id" "prompt_long") ("name" "prompt_long")) + + (div + ("class" "flex justify-between") + (div null?) + (div + ("class" "flex gap-2") + (button + ("class" "primary bold circle") + ("onclick", "globalThis.web_api_prompt_long_submit(document.getElementById('prompt_long').value); document.getElementById('prompt_long').value = ''") + ("type" "button") + (text "{{ icon \"check\" }}") + (text "{{ text \"dialog:action.okay\" }}")) + + (button + ("class" "bold red camo") + ("onclick", "globalThis.web_api_prompt_long_submit('')") + ("type" "button") + (text "{{ icon \"x\" }}") + (text "{{ text \"dialog:action.cancel\" }}"))))))) + + (dialog + ("id" "web_api_confirm") + (div + ("class" "inner flex flex-col gap-2") + (form + ("class" "flex gap-2 flex-col") + ("onsubmit" "event.preventDefault()") + (span ("id" "web_api_confirm:msg")) + + (div + ("class" "flex justify-between") + (div null?) + (div + ("class" "flex gap-2") + (button + ("class" "primary bold circle") + ("onclick", "globalThis.web_api_confirm_submit(true)") + ("type" "button") + (text "{{ icon \"check\" }}") + (text "{{ text \"dialog:action.yes\" }}")) + + (button + ("class" "bold red camo") + ("onclick", "globalThis.web_api_confirm_submit(false)") + ("type" "button") + (text "{{ icon \"x\" }}") + (text "{{ text \"dialog:action.no\" }}"))))))) + + (div + ("class" "lightbox hidden") + ("id" "lightbox") + (button + ("class" "lightbox_exit small square quaternary red") + ("onclick" "trigger('ui::lightbox_close')") + (text "{{ icon \"x\" }}")) + + (a + ("href" "") + ("id" "lightbox_img_a") + ("target" "_blank"))) + + ; tokens dialog + (text "{% if user -%}") + (dialog + ("id" "tokens_dialog") + (div + ("class" "inner flex flex-col gap-2") + (form + ("class" "flex gap-2 flex-col") + ("onsubmit" "event.preventDefault()") + (div ("id" "tokens") ("style" "display: contents")) + + (a + ("href" "/auth/login") + ("class" "button") + ("data-turbo", "false") + (text "{{ icon \"plus\" }}") + (span (text "{{ text \"general:action.add_account\" }}"))) + + (div + ("class" "flex justify-between") + (div null?) + + (div + ("class" "flex gap-2") + (button + ("class" "quaternary") + ("onclick" "document.getElementById('tokens_dialog').close()") + ("type" "button") + (text "{{ icon \"check \" }}") + (span "{{ text \"dialog:action.okay\" }}"))))))) + + ; user scripts + (text "{%- endif %} {% if user and use_user_theme -%} {{ components::theme(user=user, theme_preference=user.settings.theme_preference) }} + <script> + setTimeout(() => { + trigger(\"atto::use_theme_preference\"); + }, 150); + </script> + {%- endif %} {% if user and user.connections.Spotify and config.connections.spotify_client_id and user.connections.Spotify[0].data.token and user.connections.Spotify[0].data.refresh_token %} + <script> + setTimeout(async () => { + if (window.spotify_init) { + return; + } + + window.spotify_init = true; + const client_id = \"{{ config.connections.spotify_client_id }}\"; + let token = \"{{ user.connections.Spotify[0].data.token }}\"; + let refresh_token = + \"{{ user.connections.Spotify[0].data.refresh_token }}\"; + + if (token) { + // we already have a token + const pull_playing = async () => { + const playing = await trigger(\"spotify::get_playing\", [ + token, + ]); + + if (playing.error) { + // refresh token + const [new_token, new_refresh_token, expires_in] = + await trigger(\"spotify::refresh_token\", [ + client_id, + refresh_token, + ]); + + await trigger(\"connections::push_con_data\", [ + \"Spotify\", + { + token: new_token, + expires_in: expires_in.toString(), + name: profile.display_name, + }, + ]); + + token = new_token; + refresh_token = new_refresh_token; + return; + } + + await trigger(\"spotify::publish_playing\", [playing]); + }; + + await pull_playing(); + setInterval(pull_playing, 30_000); + } else { + window.spotify_needs_token = true; + } + }, 150); + </script> + {% elif user and user.connections.LastFm and config.connections.last_fm_key and user.connections.LastFm[0].data.session_token %} + <script> + setTimeout(async () => { + if (window.last_fm_init) { + return; + } + + window.last_fm_init = true; + const user = \"{{ user.connections.LastFm[0].data.name }}\"; + const session_token = + \"{{ user.connections.LastFm[0].data.session_token }}\"; + + if (session_token) { + // we already have a token + const pull_playing = async () => { + const playing = await trigger(\"last_fm::get_playing\", [ + user, + session_token, + ]); + await trigger(\"last_fm::publish_playing\", [playing]); + }; + + await pull_playing(); + setInterval(pull_playing, 30_000); + } else { + window.last_fm_needs_token = true; + } + }, 150); + </script> + {%- endif %}")))