2025-05-31 10:17:49 -04:00
|
|
|
(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"))
|
2025-06-01 12:25:33 -04:00
|
|
|
(link ("rel" "stylesheet") ("href" "/css/style.css"))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
(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"))
|
2025-06-01 12:25:33 -04:00
|
|
|
(meta ("property" "og:site_name") ("content" "{{ config.name }}"))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
(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")
|
2025-05-31 13:07:34 -04:00
|
|
|
(icon (text "frown"))
|
|
|
|
(str (text "general:label.account_banned")))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
(div
|
|
|
|
("class" "card")
|
2025-05-31 13:07:34 -04:00
|
|
|
(str (text "general:label.account_banned_body"))))))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
; 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\");
|
|
|
|
|
2025-06-02 20:33:51 -04:00
|
|
|
if (!atto) {
|
|
|
|
window.location.reload();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-05-31 10:17:49 -04:00
|
|
|
atto.disconnect_observers();
|
|
|
|
atto.remove_false_options();
|
|
|
|
atto.clean_date_codes();
|
2025-06-05 16:23:57 -04:00
|
|
|
atto.clean_poll_date_codes();
|
2025-05-31 10:17:49 -04:00
|
|
|
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()")
|
2025-05-31 13:07:34 -04:00
|
|
|
(icon (text "external-link"))
|
|
|
|
(str (text "dialog:action.continue")))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
(button
|
|
|
|
("class" "secondary")
|
|
|
|
("type" "button")
|
|
|
|
("onclick", "document.getElementById('link_filter').close()")
|
2025-05-31 13:07:34 -04:00
|
|
|
(icon (text "x"))
|
|
|
|
(str (text "dialog:action.cancel"))))))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
(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")
|
2025-05-31 13:07:34 -04:00
|
|
|
(icon (text "check"))
|
|
|
|
(str (text "dialog:action.okay")))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
(button
|
|
|
|
("class" "bold red camo")
|
|
|
|
("onclick", "globalThis.web_api_prompt_submit('')")
|
|
|
|
("type" "button")
|
2025-05-31 13:07:34 -04:00
|
|
|
(icon (text "x"))
|
|
|
|
(str (text "dialog:action.cancel"))))))))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
(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")
|
2025-05-31 13:07:34 -04:00
|
|
|
(icon (text "check"))
|
|
|
|
(str (text "dialog:action.okay")))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
(button
|
|
|
|
("class" "bold red camo")
|
|
|
|
("onclick", "globalThis.web_api_prompt_long_submit('')")
|
|
|
|
("type" "button")
|
2025-05-31 13:07:34 -04:00
|
|
|
(icon (text "x"))
|
|
|
|
(str (text "dialog:action.cancel"))))))))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
(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")
|
2025-05-31 13:07:34 -04:00
|
|
|
(icon (text "check"))
|
|
|
|
(str (text "dialog:action.yes")))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
(button
|
|
|
|
("class" "bold red camo")
|
|
|
|
("onclick", "globalThis.web_api_confirm_submit(false)")
|
|
|
|
("type" "button")
|
2025-05-31 13:07:34 -04:00
|
|
|
(icon (text "x"))
|
|
|
|
(str (text "dialog:action.no"))))))))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
(div
|
|
|
|
("class" "lightbox hidden")
|
|
|
|
("id" "lightbox")
|
|
|
|
(button
|
|
|
|
("class" "lightbox_exit small square quaternary red")
|
|
|
|
("onclick" "trigger('ui::lightbox_close')")
|
2025-05-31 13:07:34 -04:00
|
|
|
(icon (text "x")))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
(a
|
|
|
|
("href" "")
|
|
|
|
("id" "lightbox_img_a")
|
2025-05-31 20:42:11 -04:00
|
|
|
("target" "_blank")
|
|
|
|
(img ("id" "lightbox_img") ("loading" "lazy"))))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
; 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"))
|
|
|
|
|
|
|
|
(div
|
|
|
|
("class" "flex justify-between")
|
2025-06-02 16:11:27 -04:00
|
|
|
(a
|
|
|
|
("href" "/auth/login")
|
|
|
|
("class" "button")
|
|
|
|
("data-turbo", "false")
|
|
|
|
(icon (text "plus"))
|
|
|
|
(span (str (text "general:action.add_account"))))
|
|
|
|
|
|
|
|
(button
|
|
|
|
("class" "quaternary")
|
|
|
|
("onclick" "document.getElementById('tokens_dialog').close()")
|
|
|
|
("type" "button")
|
|
|
|
(icon (text "check")))))))
|
2025-05-31 10:17:49 -04:00
|
|
|
|
|
|
|
; 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 %}")))
|