2274 lines
98 KiB
Common Lisp
2274 lines
98 KiB
Common Lisp
(text "{% extends \"root.html\" %} {% block head %}")
|
|
(title
|
|
(text "Settings - {{ config.name }}"))
|
|
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
|
|
(main
|
|
("class" "flex flex_col gap_2")
|
|
(text "{% if profile.id != user.id -%}")
|
|
(div
|
|
("class" "card w_full red flex gap_2 items_center")
|
|
(text "{{ icon \"skull\" }}")
|
|
(b
|
|
(text "Editing other user's settings! Please be careful.")))
|
|
(text "{%- endif %}")
|
|
|
|
; nav
|
|
(div
|
|
("class" "mobile_nav mobile")
|
|
; primary nav
|
|
(div
|
|
("class" "dropdown")
|
|
("style" "width: max-content")
|
|
(button
|
|
("class" "raised small")
|
|
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
|
("exclude" "dropdown")
|
|
(icon (text "sliders-horizontal"))
|
|
(span ("class" "current_tab_text") (text "account")))
|
|
(div
|
|
("class" "inner left")
|
|
(text "{{ macros::profile_settings_nav_options() }}"))))
|
|
|
|
; nav desktop
|
|
(div
|
|
("class" "desktop pillmenu")
|
|
(text "{{ macros::profile_settings_nav_options() }}"))
|
|
|
|
; ...
|
|
(div
|
|
("class" "w_full flex flex_col gap_2 hidden")
|
|
("data-tab" "presets")
|
|
(div
|
|
("class" "card lowered flex flex_col gap_2")
|
|
(a
|
|
("href" "#/account")
|
|
("class" "button secondary")
|
|
(icon (text "arrow-left"))
|
|
(span
|
|
(str (text "general:action.back"))))
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card flex items_center gap_2 small")
|
|
(icon (text "cooking-pot"))
|
|
(span
|
|
(str (text "settings:tab.presets"))))
|
|
(div
|
|
("class" "card flex flex_col gap_2 secondary")
|
|
(p (text "Not sure where to start? Try some settings presets!"))
|
|
(details
|
|
("class" "w_full accordion")
|
|
(summary
|
|
(icon (text "rss"))
|
|
(text "Microblogging"))
|
|
|
|
(div
|
|
("class" "inner flex flex_col gap_2")
|
|
(p ("class" "fade") (text "Focus on yourself and your communities."))
|
|
(ul ("id" "preset_microblogging_ul"))
|
|
(button
|
|
("onclick" "apply_preset(PRESET_MICROBLOGGING)")
|
|
(icon (text "settings"))
|
|
(str (text "general:action.apply")))))
|
|
|
|
(details
|
|
("class" "w_full accordion")
|
|
(summary
|
|
(icon (text "message-circle-heart"))
|
|
(text "Q&A"))
|
|
|
|
(div
|
|
("class" "inner flex flex_col gap_2")
|
|
(p ("class" "fade") (text "Just like Neospring!"))
|
|
(ul ("id" "preset_questions_ul"))
|
|
(button
|
|
("onclick" "apply_preset(PRESET_QUESTIONS)")
|
|
(icon (text "settings"))
|
|
(str (text "general:action.apply")))))
|
|
|
|
(details
|
|
("class" "w_full accordion")
|
|
(summary
|
|
(icon (text "key"))
|
|
(text "Private"))
|
|
|
|
(div
|
|
("class" "inner flex flex_col gap_2")
|
|
(p ("class" "fade") (text "This preset allows you to keep your profile and posts hidden to people you aren't following."))
|
|
(ul ("id" "preset_private_ul"))
|
|
(button
|
|
("onclick" "apply_preset(PRESET_PRIVATE)")
|
|
(icon (text "settings"))
|
|
(str (text "general:action.apply")))))
|
|
|
|
(details
|
|
("class" "w_full accordion")
|
|
(summary
|
|
(icon (text "eye-closed"))
|
|
(text "NSFW"))
|
|
|
|
(div
|
|
("class" "inner flex flex_col gap_2")
|
|
(p ("class" "fade") (text "NSFW content is allowed if it is hidden from main timelines. This preset will help you do that quickly."))
|
|
(ul ("id" "preset_nsfw_ul"))
|
|
(button
|
|
("onclick" "apply_preset(PRESET_NSFW)")
|
|
(icon (text "settings"))
|
|
(str (text "general:action.apply")))))))))
|
|
|
|
(div
|
|
("class" "w_full flex flex_col gap_2")
|
|
("data-tab" "account")
|
|
(div
|
|
("class" "card lowered flex flex_col gap_2")
|
|
("id" "account_settings")
|
|
(div
|
|
("class" "pillmenu")
|
|
("ui_ident" "account_settings_tabs")
|
|
(a
|
|
("data-tab-button" "account/security")
|
|
("href" "#/account/security")
|
|
(text "{{ icon \"user-lock\" }}")
|
|
(span
|
|
(text "{{ text \"settings:tab.security\" }}")))
|
|
(a
|
|
("data-tab-button" "account/following")
|
|
("href" "#/account/following")
|
|
(text "{{ icon \"rss\" }}")
|
|
(span
|
|
(text "{{ text \"auth:label.following\" }}")))
|
|
(a
|
|
("data-tab-button" "account/followers")
|
|
("href" "#/account/followers")
|
|
(text "{{ icon \"rss\" }}")
|
|
(span
|
|
(text "{{ text \"auth:label.followers\" }}")))
|
|
(a
|
|
("data-tab-button" "account/blocks")
|
|
("href" "#/account/blocks")
|
|
(text "{{ icon \"shield\" }}")
|
|
(span
|
|
(text "{{ text \"settings:tab.blocks\" }}"))))
|
|
|
|
(text "{% if config.stripe -%}")
|
|
; stripe menu
|
|
(div
|
|
("class" "pillmenu")
|
|
("ui_ident" "account_settings_tabs")
|
|
(a
|
|
("data-tab-button" "account/uploads")
|
|
("href" "?page=0#/account/uploads")
|
|
(text "{{ icon \"image-up\" }}")
|
|
(span
|
|
(text "{{ text \"settings:tab.uploads\" }}")))
|
|
(text "{% if config.security.enable_invite_codes -%}")
|
|
(a
|
|
("data-tab-button" "account/invites")
|
|
("href" "?page=0#/account/invites")
|
|
(text "{{ icon \"ticket\" }}")
|
|
(span
|
|
(text "{{ text \"settings:tab.invites\" }}")))
|
|
(text "{%- endif %}")
|
|
(a
|
|
("data-tab-button" "account/billing")
|
|
("href" "#/account/billing")
|
|
(text "{{ icon \"credit-card\" }}")
|
|
(span
|
|
(text "{{ text \"settings:tab.billing\" }}"))))
|
|
(text "{%- endif %}")
|
|
|
|
(div
|
|
("class" "card_nest")
|
|
("ui_ident" "home_timeline")
|
|
(div
|
|
("class" "card small")
|
|
(b
|
|
(text "Home timeline")))
|
|
(div
|
|
("class" "card")
|
|
(select
|
|
("onchange" "window.SETTING_SET_FUNCTIONS[0]('default_timeline', event.target.selectedOptions[0].value.startsWith('{') ? JSON.parse(event.target.selectedOptions[0].value) : event.target.selectedOptions[0].value)")
|
|
(option
|
|
("value" "MyCommunities")
|
|
("selected" "{% if home == '/' -%}true{% else %}false{%- endif %}")
|
|
(text "My communities"))
|
|
(option
|
|
("value" "MyCommunitiesQuestions")
|
|
("selected" "{% if home == '/questions' -%}true{% else %}false{%- endif %}")
|
|
(text "My communities (questions)"))
|
|
(option
|
|
("value" "PopularPosts")
|
|
("selected" "{% if home == '/popular' -%}true{% else %}false{%- endif %}")
|
|
(text "Popular"))
|
|
(option
|
|
("value" "PopularQuestions")
|
|
("selected" "{% if home == '/popular/questions' -%}true{% else %}false{%- endif %}")
|
|
(text "Popular (questions)"))
|
|
(option
|
|
("value" "FollowingPosts")
|
|
("selected" "{% if home == '/following' -%}true{% else %}false{%- endif %}")
|
|
(text "Following"))
|
|
(option
|
|
("value" "FollowingQuestions")
|
|
("selected" "{% if home == '/following/questions' -%}true{% else %}false{%- endif %}")
|
|
(text "Following (questions)"))
|
|
(option
|
|
("value" "AllPosts")
|
|
("selected" "{% if home == '/all' -%}true{% else %}false{%- endif %}")
|
|
(text "All"))
|
|
(option
|
|
("value" "AllQuestions")
|
|
("selected" "{% if home == '/all/questions' -%}true{% else %}false{%- endif %}")
|
|
(text "All (questions)"))
|
|
(text "{% for stack in stacks %}")
|
|
(text "<option
|
|
value='{\"Stack\":\"{{ stack.id }}\"}'
|
|
selected=\"{% if home is ending_with(stack.id|as_str) -%}true{% else %}false{%- endif %}\"
|
|
>
|
|
{{ stack.name }} (stack)
|
|
</option>")
|
|
(text "{% endfor %}"))
|
|
(span
|
|
("class" "fade")
|
|
(text "This represents the timeline the home button takes you to."))))
|
|
(div
|
|
("class" "card_nest desktop")
|
|
("ui_ident" "notifications")
|
|
(div
|
|
("class" "card small")
|
|
(b
|
|
(text "Notifications")))
|
|
(div
|
|
("class" "card flex flex_col gap_2")
|
|
(button
|
|
("id" "notifications_button"))
|
|
(span
|
|
("class" "fade")
|
|
(text "Notifications require you to keep {{ config.name }}
|
|
open in your browser for real-time updates. This setting
|
|
does not sync across browsers."))))
|
|
(script
|
|
(text "setTimeout(() => {
|
|
trigger(\"me::notifications_button\", [
|
|
document.getElementById(\"notifications_button\"),
|
|
]);
|
|
}, 150);"))
|
|
(div
|
|
("class" "card_nest")
|
|
("ui_ident" "change_username")
|
|
(div
|
|
("class" "card small")
|
|
(b
|
|
(text "{{ text \"settings:label.change_username\" }}")))
|
|
(form
|
|
("class" "card flex flex_col gap_2")
|
|
("onsubmit" "change_username(event)")
|
|
(div
|
|
("class" "flex flex_col gap_1")
|
|
(label
|
|
("for" "new_username")
|
|
(text "{{ text \"settings:label.new_username\" }}"))
|
|
(input
|
|
("type" "text")
|
|
("name" "new_username")
|
|
("id" "new_username")
|
|
("placeholder" "new_username")
|
|
("required" "")
|
|
("minlength" "2")))
|
|
(button
|
|
(text "{{ icon \"check\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.save\" }}"))))))
|
|
(div
|
|
("class" "card_nest")
|
|
("ui_ident" "delete_account")
|
|
(div
|
|
("class" "card small flex items_center gap_2 red")
|
|
(icon (text "skull"))
|
|
(b (str (text "communities:label.danger_zone"))))
|
|
(div
|
|
("class" "card lowered flex flex_col gap_2")
|
|
(details
|
|
("class" "accordion")
|
|
(summary
|
|
("class" "flex items_center gap_2")
|
|
(icon_class (text "chevron-down") (text "dropdown_arrow"))
|
|
(str (text "settings:label.deactivate_account")))
|
|
(div
|
|
("class" "inner flex flex_col gap_2")
|
|
(p (text "Deactivating your account will treat it as deleted, but all your data will be recoverable if you change your mind. This option is recommended over a full deletion."))
|
|
(button
|
|
("onclick" "deactivate_account()")
|
|
(icon (text "lock"))
|
|
(span
|
|
(str (text "settings:label.deactivate"))))))
|
|
(details
|
|
("class" "accordion")
|
|
(summary
|
|
("class" "flex items_center gap_2")
|
|
(icon_class (text "chevron-down") (text "dropdown_arrow"))
|
|
(str (text "settings:label.delete_account")))
|
|
(form
|
|
("class" "inner flex flex_col gap_2")
|
|
("onsubmit" "delete_account(event)")
|
|
(div
|
|
("class" "flex flex_col gap_1")
|
|
(label
|
|
("for" "current_password")
|
|
(text "{{ text \"settings:label.current_password\" }}"))
|
|
(input
|
|
("type" "password")
|
|
("name" "current_password")
|
|
("id" "current_password")
|
|
("placeholder" "current_password")
|
|
("required" "")
|
|
("minlength" "6")
|
|
("autocomplete" "off")))
|
|
(button
|
|
(text "{{ icon \"trash\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.delete\" }}")))))))
|
|
(button
|
|
("onclick" "save_settings()")
|
|
("id" "save_button")
|
|
(text "{{ icon \"check\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.save\" }}"))))
|
|
(div
|
|
("class" "w_full flex flex_col gap_2 hidden")
|
|
("data-tab" "account/security")
|
|
(div
|
|
("class" "card lowered flex flex_col gap_2")
|
|
(a
|
|
("href" "#/account")
|
|
("class" "button secondary")
|
|
(text "{{ icon \"arrow-left\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.back\" }}")))
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card flex items_center gap_2 small")
|
|
(text "{{ icon \"user-lock\" }}")
|
|
(span
|
|
(text "{{ text \"settings:tab.security\" }}")))
|
|
(div
|
|
("class" "card flex flex_col gap_2 secondary")
|
|
(div
|
|
("class" "card_nest")
|
|
("ui_ident" "two_factor_authentication")
|
|
(div
|
|
("class" "card small")
|
|
(b
|
|
(text "{{ text \"settings:label.two_factor_authentication\" }}")))
|
|
(div
|
|
("class" "card flex flex_col gap_2")
|
|
(text "{% if profile.totp|length == 0 -%}")
|
|
(div
|
|
("id" "totp_stuff")
|
|
("style" "display: none")
|
|
(span
|
|
(text "Scan this QR code in a TOTP authenticator
|
|
app (like Google Authenticator):"))
|
|
(img
|
|
("id" "totp_qr")
|
|
("style" "max-width: 250px"))
|
|
(span
|
|
(text "TOTP secret (do NOT share):"))
|
|
(pre
|
|
("id" "totp_secret"))
|
|
(span
|
|
(text "Recovery codes (STORE SAFELY, these can
|
|
only be viewed once):"))
|
|
(pre
|
|
("id" "totp_recovery_codes")))
|
|
(button
|
|
("class" "lowered green")
|
|
("onclick" "enable_totp(event)")
|
|
(text "Enable TOTP 2FA"))
|
|
(text "{% else %}")
|
|
(pre
|
|
("id" "totp_recovery_codes")
|
|
("style" "display: none"))
|
|
(div
|
|
("class" "flex gap_2 flex_wrap")
|
|
(button
|
|
("class" "lowered red")
|
|
("onclick" "refresh_totp_codes(event)")
|
|
(text "Refresh recovery codes"))
|
|
(button
|
|
("class" "lowered red")
|
|
("onclick" "disable_totp(event)")
|
|
(text "Disable TOTP 2FA")))
|
|
(text "{%- endif %}")))
|
|
(div
|
|
("class" "card_nest")
|
|
("ui_ident" "change_password")
|
|
(div
|
|
("class" "card small")
|
|
(b
|
|
(text "{{ text \"settings:label.change_password\" }}")))
|
|
(form
|
|
("class" "card flex flex_col gap_2")
|
|
("onsubmit" "change_password(event)")
|
|
(div
|
|
("class" "flex flex_col gap_1")
|
|
(label
|
|
("for" "current_password")
|
|
(text "{{ text \"settings:label.current_password\" }}"))
|
|
(input
|
|
("type" "password")
|
|
("name" "current_password")
|
|
("id" "current_password")
|
|
("placeholder" "current_password")
|
|
("required" "")
|
|
("minlength" "6")
|
|
("autocomplete" "off")))
|
|
(div
|
|
("class" "flex flex_col gap_1")
|
|
(label
|
|
("for" "new_password")
|
|
(text "{{ text \"settings:label.new_password\" }}"))
|
|
(input
|
|
("type" "password")
|
|
("name" "new_password")
|
|
("id" "new_password")
|
|
("placeholder" "new_password")
|
|
("required" "")
|
|
("minlength" "6")
|
|
("autocomplete" "off")))
|
|
(button
|
|
(text "{{ icon \"check\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.save\" }}")))))))))
|
|
(div
|
|
("class" "w_full flex flex_col gap_2 hidden")
|
|
("data-tab" "account/following")
|
|
(div
|
|
("class" "card lowered flex flex_col gap_2")
|
|
(a
|
|
("href" "#/account")
|
|
("class" "button secondary")
|
|
(text "{{ icon \"arrow-left\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.back\" }}")))
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card flex items_center gap_2 small")
|
|
(text "{{ icon \"rss\" }}")
|
|
(span
|
|
(text "{{ text \"auth:label.following\" }}")))
|
|
(div
|
|
("class" "card flex flex_col gap_2")
|
|
(text "{% for userfollow in following %} {% set user = userfollow[1] %}")
|
|
(div
|
|
("class" "card secondary flex flex_wrap gap_2 items_center justify_between")
|
|
(div
|
|
("class" "flex gap_2")
|
|
(text "{{ components::avatar(username=user.username) }} {{ components::full_username(user=user) }}"))
|
|
(div
|
|
("class" "flex gap_2")
|
|
(button
|
|
("class" "lowered red small")
|
|
("onclick" "toggle_follow_user('{{ user.id }}')")
|
|
(text "{{ icon \"user-minus\" }}")
|
|
(span
|
|
(text "{{ text \"auth:action.unfollow\" }}")))
|
|
(a
|
|
("href" "/@{{ user.username }}")
|
|
("class" "button lowered small")
|
|
(text "{{ icon \"external-link\" }}")
|
|
(span
|
|
(text "{{ text \"requests:action.view_profile\" }}")))))
|
|
(text "{% endfor %} {{ components::pagination(page=page, items=following|length, key=\"#/account/following\") }}"))))
|
|
(script
|
|
(text "globalThis.toggle_follow_user = async (uid) => {
|
|
await trigger(\"atto::debounce\", [\"users::follow\"]);
|
|
|
|
fetch(`/api/v1/auth/user/${uid}/follow`, {
|
|
method: \"POST\",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
};")))
|
|
(div
|
|
("class" "w_full flex flex_col gap_2 hidden")
|
|
("data-tab" "account/followers")
|
|
(div
|
|
("class" "card lowered flex flex_col gap_2")
|
|
(a
|
|
("href" "#/account")
|
|
("class" "button secondary")
|
|
(text "{{ icon \"arrow-left\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.back\" }}")))
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card flex items_center gap_2 small")
|
|
(text "{{ icon \"rss\" }}")
|
|
(span
|
|
(text "{{ text \"auth:label.followers\" }}")))
|
|
(div
|
|
("class" "card flex flex_col gap_2")
|
|
(text "{% for userfollow in followers %} {% set user = userfollow[1] %}")
|
|
(div
|
|
("class" "card secondary flex flex_wrap gap_2 items_center justify_between")
|
|
(div
|
|
("class" "flex gap_2")
|
|
(text "{{ components::avatar(username=user.username) }} {{ components::full_username(user=user) }}"))
|
|
(div
|
|
("class" "flex gap_2")
|
|
(button
|
|
("class" "lowered red small")
|
|
("onclick" "force_unfollow_me('{{ user.id }}')")
|
|
(text "{{ icon \"user-minus\" }}")
|
|
(span
|
|
(str (text "stacks:label.remove"))))
|
|
(a
|
|
("href" "/@{{ user.username }}")
|
|
("class" "button lowered small")
|
|
(text "{{ icon \"external-link\" }}")
|
|
(span
|
|
(text "{{ text \"requests:action.view_profile\" }}")))))
|
|
(text "{% endfor %} {{ components::pagination(page=page, items=following|length, key=\"#/account/followers\") }}"))))
|
|
(script
|
|
(text "globalThis.force_unfollow_me = async (uid) => {
|
|
await trigger(\"atto::debounce\", [\"users::follow\"]);
|
|
|
|
fetch(`/api/v1/auth/user/${uid}/force_unfollow_me`, {
|
|
method: \"POST\",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
};")))
|
|
(div
|
|
("class" "w_full flex flex_col gap_2 hidden")
|
|
("data-tab" "account/blocks")
|
|
(div
|
|
("class" "card lowered flex flex_col gap_2")
|
|
(a
|
|
("href" "#/account")
|
|
("class" "button secondary")
|
|
(text "{{ icon \"arrow-left\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.back\" }}")))
|
|
|
|
; stack blocks
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card flex items_center gap_2 small")
|
|
(text "{{ icon \"layers\" }}")
|
|
(span
|
|
(text "{{ text \"stacks:link.stacks\" }}")))
|
|
(div
|
|
("class" "card flex flex_col gap_2")
|
|
(text "{% for stack in stackblocks %}")
|
|
(text "{{ components::stack_listing(stack=stack) }}")
|
|
(text "{% endfor %}")))
|
|
|
|
; user blocks
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card flex items_center gap_2 small")
|
|
(text "{{ icon \"users-round\" }}")
|
|
(span
|
|
(text "{{ text \"settings:label.users\" }}")))
|
|
(div
|
|
("class" "card flex flex_col gap_2")
|
|
(text "{% for user in blocks %}")
|
|
(div
|
|
("class" "card secondary flex flex_wrap gap_2 items_center justify_between")
|
|
(div
|
|
("class" "flex gap_2")
|
|
(text "{{ components::avatar(username=user.username) }} {{ components::full_username(user=user) }}"))
|
|
(div
|
|
("class" "flex gap_2")
|
|
(a
|
|
("href" "/stacks/add_user/{{ user.id }}")
|
|
("target" "_blank")
|
|
("class" "button lowered small")
|
|
(icon (text "plus"))
|
|
(span (str (text "settings:label.add_to_stack"))))
|
|
(a
|
|
("href" "/@{{ user.username }}")
|
|
("class" "button lowered small")
|
|
(icon (text "external-link"))
|
|
(span (str (text "requests:action.view_profile"))))))
|
|
(text "{% endfor %}")))
|
|
|
|
; ip blocks
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card flex items_center gap_2 small")
|
|
(text "{{ icon \"wifi\" }}")
|
|
(span
|
|
(text "{{ text \"settings:label.ips\" }}")))
|
|
(div
|
|
("class" "card flex flex_col gap_2")
|
|
(text "{% for ip in ipblocks %}")
|
|
(div
|
|
("class" "card secondary flex flex_wrap gap_2 items_center justify_between")
|
|
(span
|
|
(text "Block from: ") (span ("class" "date") (text "{{ ip.created }}")))
|
|
(div
|
|
("class" "flex gap_2")
|
|
(button
|
|
("onclick" "trigger('me::remove_ip_block', ['{{ ip.id }}'])")
|
|
("class" "lowered small red")
|
|
(icon (text "x"))
|
|
(span (str (text "auth:action.unblock"))))))
|
|
(text "{% endfor %}")))))
|
|
(div
|
|
("class" "w_full flex flex_col gap_2 hidden")
|
|
("data-tab" "account/uploads")
|
|
(div
|
|
("class" "card lowered flex flex_col gap_2")
|
|
(a
|
|
("href" "#/account")
|
|
("class" "button secondary")
|
|
(text "{{ icon \"arrow-left\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.back\" }}")))
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card flex items_center gap_2 small")
|
|
(text "{{ icon \"image-up\" }}")
|
|
(span
|
|
(text "{{ text \"settings:tab.uploads\" }}")))
|
|
(div
|
|
("class" "card flex flex_col gap_2 secondary")
|
|
(text "{{ components::supporter_ad(body=\"Become a supporter to upload images directly to posts!\") }} {% for upload in uploads %}")
|
|
(details
|
|
("class" "accordion w_full")
|
|
(summary
|
|
("class" "card flex flex_wrap gap_2 items_center justify_between")
|
|
(div
|
|
("class" "flex gap_2 items_center")
|
|
(icon_class (text "chevron-down") (text "dropdown_arrow"))
|
|
(b
|
|
(span
|
|
("class" "date")
|
|
(text "{{ upload.created }}"))
|
|
(text " ({{ upload.what }})")))
|
|
(div
|
|
("class" "flex gap_2")
|
|
(button
|
|
("class" "raised small")
|
|
("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload.id }}'])")
|
|
(text "{{ icon \"view\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.view\" }}")))
|
|
(button
|
|
("class" "raised small red")
|
|
("onclick" "remove_upload('{{ upload.id }}')")
|
|
(text "{{ icon \"x\" }}")
|
|
(span
|
|
(text "{{ text \"stacks:label.remove\" }}")))))
|
|
|
|
(div
|
|
("class" "inner flex flex_col gap_2")
|
|
(form
|
|
("class" "card lowered flex flex_col gap_2")
|
|
("onsubmit" "update_upload_alt(event, '{{ upload.id }}')")
|
|
(div
|
|
("class" "flex flex_col gap_1")
|
|
(label ("for" "alt_{{ upload.id }}") (b (str (text "settings:label.alt_text"))))
|
|
(textarea
|
|
("id" "alt_{{ upload.id }}")
|
|
("name" "alt")
|
|
("class" "w_full")
|
|
("placeholder" "Alternative text")
|
|
(text "{{ upload.alt|safe }}")))
|
|
|
|
(button
|
|
(icon (text "check"))
|
|
(str (text "general:action.save"))))))
|
|
(text "{% endfor %} {{ components::pagination(page=page, items=uploads|length, key=\"#/account/uploads\") }}")
|
|
(script
|
|
(text "globalThis.remove_upload = async (id) => {
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you would like to do this? This action is permanent.\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/v1/uploads/${id}`, {
|
|
method: \"DELETE\",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
};
|
|
|
|
globalThis.update_upload_alt = async (e, id) => {
|
|
e.preventDefault();
|
|
fetch(`/api/v1/uploads/${id}/alt`, {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
alt: e.target.alt.value,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
};"))))))
|
|
|
|
(text "{% if config.security.enable_invite_codes -%}")
|
|
(div
|
|
("class" "w_full flex flex_col gap_2 hidden")
|
|
("data-tab" "account/invites")
|
|
(div
|
|
("class" "card lowered flex flex_col gap_2")
|
|
(a
|
|
("href" "#/account")
|
|
("class" "button secondary")
|
|
(text "{{ icon \"arrow-left\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.back\" }}")))
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card flex items_center gap_2 small")
|
|
(text "{{ icon \"ticket\" }}")
|
|
(span
|
|
(text "{{ text \"settings:tab.invites\" }}")))
|
|
(div
|
|
("class" "card flex flex_col gap_2 secondary")
|
|
(pre ("id" "invite_codes_output") ("class" "hidden") (code))
|
|
(pre ("id" "invite_codes_error_output") ("class" "hidden") (code ("class" "red")))
|
|
|
|
(button
|
|
("onclick" "generate_invite_codes()")
|
|
(icon (text "plus"))
|
|
(str (text "settings:label.generate_invites")))
|
|
|
|
(text "{{ components::supporter_ad(body=\"Become a supporter to generate up to 48 invite codes! You can currently have 2 maximum.\") }} {% for code in invites %}")
|
|
(div
|
|
("class" "card flex flex_col gap_2")
|
|
(text "{% if code[1].is_used -%}")
|
|
; used
|
|
(b ("class" "{% if code[1].is_used -%} fade {%- endif %}") (s (text "{{ code[1].code }}")))
|
|
(text "{{ components::full_username(user=code[0]) }}")
|
|
(text "{% else %}")
|
|
; unused
|
|
(b (text "{{ code[1].code }}"))
|
|
(text "{%- endif %}"))
|
|
(text "{% endfor %}")
|
|
(text "{{ components::pagination(page=page, items=invites|length, key=\"#/account/invites\") }}")
|
|
(script
|
|
(text "globalThis.generate_invite_codes = async () => {
|
|
await trigger(\"atto::debounce\", [\"invites::create\"]);
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you would like to do this? This action is permanent.\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const count = Number.parseInt(await trigger(\"atto::prompt\", [\"Count (1-48):\"]));
|
|
|
|
if (!count) {
|
|
return;
|
|
}
|
|
|
|
document.getElementById(\"invite_codes_output\").classList.remove(\"hidden\");
|
|
document.getElementById(\"invite_codes_error_output\").classList.remove(\"hidden\");
|
|
document.getElementById(\"invite_codes_output\").children[0].innerText = \"Working... expect to wait 50ms per invite code\";
|
|
|
|
fetch(`/api/v1/invites/${count}`, {
|
|
method: \"POST\",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
|
|
if (res.ok) {
|
|
document.getElementById(\"invite_codes_output\").children[0].innerText = res.payload[0];
|
|
document.getElementById(\"invite_codes_error_output\").children[0].innerText = res.payload[1];
|
|
}
|
|
});
|
|
};"))))))
|
|
(text "{%- endif %}")
|
|
|
|
(div
|
|
("class" "w_full flex flex_col gap_2 hidden")
|
|
("data-tab" "account/billing")
|
|
(div
|
|
("class" "card lowered flex flex_col gap_2")
|
|
(a
|
|
("href" "#/account")
|
|
("class" "button secondary")
|
|
(text "{{ icon \"arrow-left\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.back\" }}")))
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card flex items_center gap_2 small")
|
|
(text "{{ icon \"credit-card\" }}")
|
|
(span
|
|
(text "{{ text \"settings:tab.billing\" }}")))
|
|
(div
|
|
("class" "card flex flex_col gap_2 secondary")
|
|
(text "{% if config.stripe -%}")
|
|
(text "{% if has_developer_pass or is_supporter -%}")
|
|
(div
|
|
("class" "card_nest")
|
|
("ui_ident" "supporter_card")
|
|
(div
|
|
("class" "card small flex items_center gap_2")
|
|
(icon (text "credit-card"))
|
|
(b
|
|
(text "Manage billing")))
|
|
(div
|
|
("class" "card flex flex_col gap_2")
|
|
(p
|
|
(text "You currently have a subscription! You can manage your billing information below. ")
|
|
(b
|
|
(text "Please use your email address you supplied when paying to log into the billing portal."))
|
|
(text " You can manage all of your active subscriptions through this page."))
|
|
(a
|
|
("href" "{{ config.stripe.billing_portal_url }}")
|
|
("class" "button lowered")
|
|
("target" "_blank")
|
|
(text "Manage billing"))))
|
|
(text "{%- endif %}")
|
|
|
|
(div
|
|
("class" "card_nest")
|
|
("ui_ident" "supporter_card")
|
|
(div
|
|
("class" "card small flex items_center gap_2")
|
|
(text "{{ icon \"star\" }}")
|
|
(b
|
|
(text "Supporter status")))
|
|
(div
|
|
("class" "card flex flex_col gap_2 no_p_margin")
|
|
(text "{% if is_supporter -%}")
|
|
(p
|
|
(text "You ")
|
|
(b (text "are "))
|
|
(text "a supporter! Thank you for all that you do."))
|
|
(text "{% else %}")
|
|
(text "{{ components::become_supporter_button() }}")
|
|
(text "{%- endif %}")))
|
|
|
|
(div
|
|
("class" "card_nest")
|
|
("ui_ident" "supporter_card")
|
|
(div
|
|
("class" "card small flex items_center gap_2")
|
|
(icon (text "id-card-lanyard"))
|
|
(b
|
|
(text "Developer pass status")))
|
|
(div
|
|
("class" "card flex flex_col gap_2 no_p_margin")
|
|
(text "{% if has_developer_pass -%}")
|
|
(p
|
|
(text "You currently have a developer pass!"))
|
|
(text "{% else %}")
|
|
(text "{{ components::get_developer_pass_button() }}")
|
|
(text "{%- endif %}")))
|
|
|
|
(text "{% if user.was_purchased and user.invite_code == 0 -%}")
|
|
(form
|
|
("class" "card w_full lowered flex flex_col gap_2")
|
|
("onsubmit" "update_invite_code(event)")
|
|
(p (text "Your account is currently activated without an invite code. If you stop paying for supporter, your account will be locked again until you renew. You can provide an invite code to avoid this if you're planning on cancelling."))
|
|
|
|
(div
|
|
("class" "flex flex_col gap_1")
|
|
(label
|
|
("for" "invite_code")
|
|
(b
|
|
(text "Invite code")))
|
|
(input
|
|
("type" "text")
|
|
("placeholder" "invite code")
|
|
("name" "invite_code")
|
|
("required" "")
|
|
("id" "invite_code")))
|
|
|
|
(button
|
|
(text "Submit")))
|
|
(text "{%- endif %}")
|
|
(text "{%- endif %}")))))
|
|
(div
|
|
("class" "w_full hidden flex flex_col gap_2")
|
|
("data-tab" "profile")
|
|
(div
|
|
("class" "card lowered flex flex_col gap_2")
|
|
("id" "profile_settings")
|
|
(text "{{ components::supporter_ad(body=\"Become a supporter to upload GIF images!\") }}")
|
|
(div
|
|
("class" "card_nest")
|
|
("ui_ident" "change_avatar")
|
|
(div
|
|
("class" "card small")
|
|
(b
|
|
(text "{{ text \"settings:label.change_avatar\" }}")))
|
|
(form
|
|
("class" "card flex gap_2 flex_row flex_wrap items_center")
|
|
("method" "post")
|
|
("enctype" "multipart/form-data")
|
|
("onsubmit" "upload_avatar(event)")
|
|
(div
|
|
("class" "flex gap_2 flex_row flex_wrap items_center")
|
|
(input
|
|
("id" "avatar_file")
|
|
("name" "file")
|
|
("type" "file")
|
|
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
|
("class" "w_content"))
|
|
(button
|
|
(text "{{ icon \"check\" }}")))
|
|
(span
|
|
("class" "fade")
|
|
(text "Images must be less than 8 MB large. Animated GIFs are
|
|
only supported for supporter users. GIFs can be at most
|
|
2 MB large."))))
|
|
(div
|
|
("class" "card_nest")
|
|
("ui_ident" "change_banner")
|
|
(div
|
|
("class" "card small")
|
|
(b
|
|
(text "{{ text \"settings:label.change_banner\" }}")))
|
|
(form
|
|
("class" "card flex flex_col gap_2")
|
|
("method" "post")
|
|
("enctype" "multipart/form-data")
|
|
("onsubmit" "upload_banner(event)")
|
|
(div
|
|
("class" "flex gap_2 flex_row flex_wrap items_center")
|
|
(input
|
|
("id" "banner_file")
|
|
("name" "file")
|
|
("type" "file")
|
|
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
|
("class" "w_content"))
|
|
(button
|
|
(text "{{ icon \"check\" }}")))
|
|
(span
|
|
("class" "fade")
|
|
(text "Use an image of 1100x350px for the best results."))))
|
|
(div
|
|
("class" "card_nest")
|
|
("ui_ident" "default_profile_page")
|
|
(div
|
|
("class" "card small")
|
|
(b
|
|
(text "Default profile tab")))
|
|
(div
|
|
("class" "card")
|
|
(select
|
|
("onchange" "window.SETTING_SET_FUNCTIONS[0]('default_profile_tab', event.target.selectedOptions[0].value)")
|
|
(option
|
|
("value" "Posts")
|
|
("selected" "{% if profile.settings.default_profile_tab == 'Posts' -%}true{% else %}false{%- endif %}")
|
|
(text "Posts"))
|
|
(option
|
|
("value" "Responses")
|
|
("selected" "{% if profile.settings.default_profile_tab == 'Responses' -%}true{% else %}false{%- endif %}")
|
|
(text "Responses")))
|
|
(span
|
|
("class" "fade")
|
|
(text "This represents the timeline that is shown on your profile by default."))))
|
|
(div
|
|
("class" "flex flex_col gap_2")
|
|
("ui_ident" "show_presets")
|
|
(hr ("class" "margin"))
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card small")
|
|
(b
|
|
(text "Not sure what to do?")))
|
|
(div
|
|
("class" "card no_p_margin")
|
|
(p
|
|
(text "Quickly set up your account with ")
|
|
(a ("href" "/settings#/presets") (text "settings presets"))
|
|
(text "!"))))))
|
|
(button
|
|
("onclick" "save_settings()")
|
|
("id" "save_button")
|
|
(text "{{ icon \"check\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.save\" }}"))))
|
|
(div
|
|
("class" "card w_full lowered hidden flex flex_col gap_2")
|
|
("data-tab" "sessions")
|
|
(text "{% for token in profile.tokens %}")
|
|
(div
|
|
("class" "card w_full flex justify_between flex_collapse gap_2")
|
|
(div
|
|
("class" "flex flex_col gap_1")
|
|
(b
|
|
("style" "
|
|
width: 200px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
")
|
|
(text "{{ token[1] }}"))
|
|
(text "{% if is_helper -%}")
|
|
(span
|
|
("class" "flex gap_2 items_center")
|
|
(span
|
|
("class" "fade")
|
|
(a
|
|
("href" "/api/v1/auth/user/find_by_ip/{{ token[0] }}")
|
|
(code
|
|
(text "{{ token[0] }}")))))
|
|
(text "{% else %}")
|
|
(span
|
|
("class" "fade")
|
|
(code
|
|
(text "{{ token[0] }}")))
|
|
(text "{%- endif %}")
|
|
(span
|
|
("class" "fade date")
|
|
(text "{{ token[2] }}")))
|
|
(button
|
|
("class" "lowered red")
|
|
("onclick" "remove_token('{{ token[1] }}')")
|
|
(text "{{ text \"general:action.delete\" }}")))
|
|
(text "{% endfor %}"))
|
|
(div
|
|
("class" "w_full hidden flex flex_col gap_2")
|
|
("data-tab" "theme")
|
|
(div
|
|
("class" "card lowered flex flex_col gap_2")
|
|
("id" "theme_settings")
|
|
(text "{% if failing_color_keys|length > 0 -%}")
|
|
(div
|
|
("class" "card flex flex_col gap_2")
|
|
("style" "background: white; color: black")
|
|
("ui_ident" "awful_contrast")
|
|
(div
|
|
("class" "flex gap_2 items_center")
|
|
(span
|
|
("class" "desktop")
|
|
("style" "display: contents")
|
|
(text "{{ icon \"contrast\" }}"))
|
|
(b
|
|
(text "Some of your custom colors fail contrast checks:")))
|
|
(ul
|
|
(text "{% for key in failing_color_keys %}")
|
|
(li
|
|
(text "{{ key[0] }} ")
|
|
(b
|
|
(text "{{ key[1] }} < 4.5")))
|
|
(text "{% endfor %}")))
|
|
(text "{%- endif %}")
|
|
(div
|
|
("class" "card w_full flex flex_wrap gap_2")
|
|
("ui_ident" "import_export")
|
|
(button
|
|
("onclick" "import_theme_settings()")
|
|
(text "{{ icon \"upload\" }}")
|
|
(span
|
|
(text "{{ text \"settings:label.import\" }}")))
|
|
(button
|
|
("class" "secondary")
|
|
("onclick" "export_theme_settings()")
|
|
(text "{{ icon \"download\" }}")
|
|
(span
|
|
(text "{{ text \"settings:label.export\" }}"))))
|
|
(text "{{ components::supporter_ad(body=\"Become a supporter to add custom CSS!\") }}")
|
|
(div
|
|
("class" "card_nest")
|
|
("ui_ident" "theme_preference")
|
|
(div
|
|
("class" "card small")
|
|
(b
|
|
(text "Theme preference")))
|
|
(div
|
|
("class" "card")
|
|
(select
|
|
("onchange" "window.SETTING_SET_FUNCTIONS[0]('theme_preference', event.target.selectedOptions[0].value)")
|
|
(option
|
|
("value" "Auto")
|
|
("selected" "{% if user.settings.theme_preference == 'Auto' -%}true{% else %}false{%- endif %}")
|
|
(text "Auto"))
|
|
(option
|
|
("value" "Light")
|
|
("selected" "{% if user.settings.theme_preference == 'Light' -%}true{% else %}false{%- endif %}")
|
|
(text "Light"))
|
|
(option
|
|
("value" "Dark")
|
|
("selected" "{% if user.settings.theme_preference == 'Dark' -%}true{% else %}false{%- endif %}")
|
|
(text "Dark")))
|
|
(span
|
|
("class" "fade")
|
|
(text "This represents your local site theme."))))
|
|
(div
|
|
("class" "card_nest")
|
|
("ui_ident" "profile_theme")
|
|
(div
|
|
("class" "card small")
|
|
(b
|
|
(text "Profile theme base")))
|
|
(div
|
|
("class" "card")
|
|
(select
|
|
("onchange" "window.SETTING_SET_FUNCTIONS[0]('profile_theme', event.target.selectedOptions[0].value)")
|
|
(option
|
|
("value" "Auto")
|
|
("selected" "{% if user.settings.profile_theme == 'Auto' -%}true{% else %}false{%- endif %}")
|
|
(text "Auto"))
|
|
(option
|
|
("value" "Light")
|
|
("selected" "{% if user.settings.profile_theme == 'Light' -%}true{% else %}false{%- endif %}")
|
|
(text "Light"))
|
|
(option
|
|
("value" "Dark")
|
|
("selected" "{% if user.settings.profile_theme == 'Dark' -%}true{% else %}false{%- endif %}")
|
|
(text "Dark")))
|
|
(span
|
|
("class" "fade")
|
|
(text "This represents the site theme shown to users viewing
|
|
your profile.")))))
|
|
(button
|
|
("onclick" "save_settings()")
|
|
("id" "save_button")
|
|
(text "{{ icon \"check\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.save\" }}"))))
|
|
(div
|
|
("class" "card w_full lowered hidden flex flex_col gap_2")
|
|
("data-tab" "connections")
|
|
(div
|
|
("class" "card w_full flex flex_wrap gap_2")
|
|
(text "{% if config.connections.spotify_client_id and not profile.connections.Spotify %}")
|
|
(button
|
|
("class" "lowered")
|
|
("onclick" "trigger('spotify::create_connection', ['{{ config.connections.spotify_client_id }}'])")
|
|
(text "{{ icon \"spotify\" }}")
|
|
(span
|
|
(text "Spotify")))
|
|
(text "{%- endif %} {% if config.connections.last_fm_key and not profile.connections.LastFm %}")
|
|
(button
|
|
("class" "lowered")
|
|
("onclick" "trigger('last_fm::create_connection', ['{{ config.connections.last_fm_key }}'])")
|
|
(text "{{ icon \"last_fm\" }}")
|
|
(span
|
|
(text "Last.fm")))
|
|
(text "{%- endif %}"))
|
|
(text "{% for key, value in profile.connections %}")
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card small flex items_center gap_2")
|
|
(text "{{ components::connection_icon(key=key) }}")
|
|
(b
|
|
("class" "flex items_center gap_2")
|
|
(text "{% if value[0].data.name -%}")
|
|
(span
|
|
(text "{{ value[0].data.name }}"))
|
|
(span
|
|
("style" "display: contents;")
|
|
("title" "Verified connection")
|
|
(text "{{ icon \"badge-check\" }}"))
|
|
(text "{% else %}")
|
|
(span
|
|
(text "{{ key }}"))
|
|
(span
|
|
("style" "display: contents;")
|
|
(text "{{ icon \"badge-alert\" }}"))
|
|
(text "{%- endif %}")))
|
|
(div
|
|
("class" "card flex flex_col gap_2")
|
|
(button
|
|
("class" "lowered red small")
|
|
("onclick" "trigger('connections::delete', ['{{ key }}'])")
|
|
(text "{{ text \"general:action.delete\" }}"))
|
|
(label
|
|
("for" "{{ key }}-shown")
|
|
("class" "flex items_center gap_2")
|
|
(input
|
|
("type" "checkbox")
|
|
("checked" "{% if value[0].show_on_profile -%}true{% else %}false{%- endif %}")
|
|
("id" "{{ key }}-shown")
|
|
("onchange" "trigger('connections::push_con_shown', ['{{ key }}', event.target.checked])")
|
|
("class" "w_content"))
|
|
(span
|
|
(text "Shown on profile")))))
|
|
(text "{% endfor %}")
|
|
(text "{% for grant in profile_grants %}")
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card small flex items_center gap_4")
|
|
(div
|
|
("class" "flex items_center gap_2")
|
|
(icon (text "bot"))
|
|
(a
|
|
("class" "flush")
|
|
("href" "{{ grant[0].homepage }}")
|
|
("target" "_blank")
|
|
(b
|
|
("class" "flex items_center gap_2")
|
|
(text "{{ grant[0].title }}"))))
|
|
|
|
(span
|
|
("class" "fade flex items_center gap_2")
|
|
(icon (text "clock"))
|
|
(span ("class" "date") (text "{{ grant[1].last_updated }}"))))
|
|
(div
|
|
("class" "card flex flex_col gap_2")
|
|
(details
|
|
(summary (icon (text "scan-eye")) (text "{{ grant[1].scopes|length }} scope{{ grant[1].scopes|length|pluralize }}"))
|
|
|
|
(div
|
|
("class" "card lowered w_full")
|
|
(ul
|
|
(text "{% for scope in grant[1].scopes -%}")
|
|
(li
|
|
(a
|
|
("href" "https://tetratto.com/reference/tetratto/model/oauth/enum.AppScope.html#variant.{{ scope }}")
|
|
("target" "_blank")
|
|
(text "{{ scope }}")))
|
|
(text "{%- endfor %}"))))
|
|
|
|
(button
|
|
("class" "lowered red small")
|
|
("onclick" "remove_grant('{{ grant[0].id }}')")
|
|
(text "{{ text \"general:action.delete\" }}"))))
|
|
(text "{% endfor %}")
|
|
|
|
(hr)
|
|
(a
|
|
("class" "button")
|
|
("href" "/developer")
|
|
(icon (text "code"))
|
|
(span
|
|
(text "{{ config.name }} ")
|
|
(str (text "developer:label.for_developers")))))
|
|
(script
|
|
("type" "application/json")
|
|
("id" "settings_json")
|
|
(text "{{ profile.settings|json_encode()|remove_script_tags|safe }}"))
|
|
(script
|
|
(text "setTimeout(async () => {
|
|
const ui = await ns(\"ui\");
|
|
const settings = JSON.parse(
|
|
document.getElementById(\"settings_json\").innerHTML,
|
|
);
|
|
let tokens = JSON.parse(\"{{ user_tokens_serde|safe }}\");
|
|
|
|
globalThis.remove_token = async (id) => {
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you would like to do this?\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// reconstruct tokens (but without the token with the given id)
|
|
const new_tokens = [];
|
|
|
|
for (const token of tokens) {
|
|
if (token[1] === id) {
|
|
continue;
|
|
}
|
|
|
|
new_tokens.push(token);
|
|
}
|
|
|
|
tokens = new_tokens;
|
|
|
|
// send request to save
|
|
fetch(\"/api/v1/auth/user/{{ profile.id }}/tokens\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify(tokens),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
};
|
|
|
|
globalThis.remove_grant = async (id) => {
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you would like to do this?\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/v1/auth/user/{{ profile.id }}/grants/${id}`, {
|
|
method: \"DELETE\",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
};
|
|
|
|
globalThis.save_settings = () => {
|
|
fetch(\"/api/v1/auth/user/{{ profile.id }}/settings\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify(settings),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
};
|
|
|
|
globalThis.change_password = (e) => {
|
|
e.preventDefault();
|
|
fetch(\"/api/v1/auth/user/{{ profile.id }}/password\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
from: e.target.current_password.value,
|
|
to: e.target.new_password.value,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
};
|
|
|
|
globalThis.change_username = async (e) => {
|
|
e.preventDefault();
|
|
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you would like to do this?\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(\"/api/v1/auth/user/{{ profile.id }}/username\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
to: e.target.new_username.value,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
};
|
|
|
|
globalThis.delete_account = async (e) => {
|
|
e.preventDefault();
|
|
|
|
// {% if user.permissions|has_supporter %}
|
|
alert(\"Please cancel your membership before deleting your account. You'll have to wait until the next cycle to delete your account after, or you can request support if it is urgent.\");
|
|
return;
|
|
// {% endif %}
|
|
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you would like to do this?\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(\"/api/v1/auth/user/{{ profile.id }}\", {
|
|
method: \"DELETE\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
password: e.target.current_password.value,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
};
|
|
|
|
globalThis.upload_avatar = (e) => {
|
|
e.preventDefault();
|
|
e.target.querySelector(\"button\").style.display = \"none\";
|
|
|
|
fetch(\"/api/v1/auth/upload/avatar\", {
|
|
method: \"POST\",
|
|
body: e.target.file.files[0],
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
|
|
e.target
|
|
.querySelector(\"button\")
|
|
.removeAttribute(\"style\");
|
|
});
|
|
|
|
alert(\"Avatar upload in progress. Please wait!\");
|
|
};
|
|
|
|
globalThis.upload_banner = (e) => {
|
|
e.preventDefault();
|
|
e.target.querySelector(\"button\").style.display = \"none\";
|
|
|
|
fetch(\"/api/v1/auth/upload/banner\", {
|
|
method: \"POST\",
|
|
body: e.target.file.files[0],
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
|
|
e.target
|
|
.querySelector(\"button\")
|
|
.removeAttribute(\"style\");
|
|
});
|
|
|
|
alert(\"Banner upload in progress. Please wait!\");
|
|
};
|
|
|
|
globalThis.enable_totp = async (event) => {
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you want to do this? You must have access to your TOTP codes to disable TOTP.\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(\"/api/v1/auth/user/{{ user.id }}/totp\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
|
|
const [secret, qr, recovery_codes] = res.payload;
|
|
|
|
document.getElementById(\"totp_secret\").innerText =
|
|
secret;
|
|
document.getElementById(\"totp_qr\").src =
|
|
`data:image/png;base64,${qr}`;
|
|
document.getElementById(
|
|
\"totp_recovery_codes\",
|
|
).innerText = recovery_codes.join(\"\n\");
|
|
|
|
document.getElementById(\"totp_stuff\").style.display =
|
|
\"contents\";
|
|
event.target.remove();
|
|
});
|
|
};
|
|
|
|
globalThis.disable_totp = async (event) => {
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you want to do this?\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const totp_code = await trigger(\"atto::prompt\", [\"TOTP code:\"]);
|
|
|
|
if (!totp_code) {
|
|
return;
|
|
}
|
|
|
|
fetch(\"/api/v1/auth/user/{{ profile.id }}/totp\", {
|
|
method: \"DELETE\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({ totp: totp_code }),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
|
|
event.target.remove();
|
|
});
|
|
};
|
|
|
|
globalThis.refresh_totp_codes = async (event) => {
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you want to do this? The old codes will no longer work.\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const totp_code = await trigger(\"atto::prompt\", [\"TOTP code:\"]);
|
|
|
|
if (!totp_code) {
|
|
return;
|
|
}
|
|
|
|
fetch(\"/api/v1/auth/user/{{ profile.id }}/totp/codes\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({ totp: totp_code }),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
|
|
document.getElementById(
|
|
\"totp_recovery_codes\",
|
|
).innerText = res.payload.join(\"\n\");
|
|
document.getElementById(
|
|
\"totp_recovery_codes\",
|
|
).style.display = \"block\";
|
|
|
|
event.target.remove();
|
|
});
|
|
};
|
|
|
|
globalThis.update_invite_code = async (e) => {
|
|
e.preventDefault();
|
|
await trigger(\"atto::debounce\", [\"invite_codes::try\"]);
|
|
fetch(\"/api/v1/auth/user/me/invite_code\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
invite_code: e.target.invite_code.value,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
|
|
if (res.ok) {
|
|
window.location.reload();
|
|
}
|
|
});
|
|
}
|
|
|
|
globalThis.deactivate_account = async () => {
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you want to do this?\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(\"/api/v1/auth/user/{{ profile.id }}/deactivate\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({ is_deactivated: true }),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
};
|
|
|
|
// presets
|
|
globalThis.apply_preset = async (preset) => {
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you would like to do this? This will change all listed settings to their listed values.\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
for (const x of preset) {
|
|
window.SETTING_SET_FUNCTIONS[0](x[0], x[1])
|
|
}
|
|
|
|
save_settings();
|
|
}
|
|
|
|
globalThis.render_preset_lis = (preset, id) => {
|
|
for (const x of preset) {
|
|
document.getElementById(id).innerHTML += `<li><b>${x[0]}:</b> ${x[1]}</li>`;
|
|
}
|
|
}
|
|
|
|
globalThis.PRESET_MICROBLOGGING = [
|
|
[\"default_timeline\", \"All\"],
|
|
[\"all_timeline_hide_answers\", true],
|
|
];
|
|
|
|
globalThis.PRESET_QUESTIONS = [
|
|
[\"default_timeline\", \"Following\"],
|
|
[\"auto_full_unlist\", true],
|
|
[\"enable_questions\", true],
|
|
[\"allow_anonymous_questions\", true],
|
|
[\"enable_drawings\", true],
|
|
[\"hide_extra_post_tabs\", true],
|
|
];
|
|
|
|
globalThis.PRESET_PRIVATE = [
|
|
[\"private_profile\", true],
|
|
[\"private_last_seen\", true],
|
|
[\"private_communities\", true],
|
|
[\"private_chats\", true],
|
|
[\"private_mails\", true],
|
|
[\"require_account\", true],
|
|
];
|
|
|
|
globalThis.PRESET_NSFW = [
|
|
[\"auto_unlist\", true],
|
|
[\"show_nsfw\", true],
|
|
];
|
|
|
|
render_preset_lis(PRESET_MICROBLOGGING, \"preset_microblogging_ul\");
|
|
render_preset_lis(PRESET_QUESTIONS, \"preset_questions_ul\");
|
|
render_preset_lis(PRESET_PRIVATE, \"preset_private_ul\");
|
|
render_preset_lis(PRESET_NSFW, \"preset_nsfw_ul\");
|
|
|
|
// ...
|
|
const account_settings =
|
|
document.getElementById(\"account_settings\");
|
|
const profile_settings =
|
|
document.getElementById(\"profile_settings\");
|
|
const theme_settings = document.getElementById(\"theme_settings\");
|
|
|
|
ui.refresh_container(account_settings, [
|
|
\"supporter_ad\",
|
|
\"account_settings_tabs\",
|
|
\"home_timeline\",
|
|
\"notifications\",
|
|
\"change_username\",
|
|
\"delete_account\",
|
|
]);
|
|
ui.refresh_container(profile_settings, [
|
|
\"supporter_ad\",
|
|
\"change_avatar\",
|
|
\"change_banner\",
|
|
\"default_profile_page\",
|
|
\"show_presets\",
|
|
]);
|
|
ui.refresh_container(theme_settings, [
|
|
\"supporter_ad\",
|
|
\"awful_contrast\",
|
|
\"import_export\",
|
|
\"theme_preference\",
|
|
\"profile_theme\",
|
|
]);
|
|
|
|
ui.generate_settings_ui(
|
|
account_settings,
|
|
[
|
|
[
|
|
[\"display_name\", \"Display name\"],
|
|
\"{{ profile.settings.display_name }}\",
|
|
\"input\",
|
|
],
|
|
[
|
|
[\"biography\", \"Biography\"],
|
|
settings.biography,
|
|
\"textarea\",
|
|
],
|
|
[
|
|
[\"private_biography\", \"Private biography\"],
|
|
settings.private_biography,
|
|
\"textarea\",
|
|
{
|
|
embed_html:
|
|
'<span class=\"fade\">This biography is only shown to users you are not following while your account is private.</span>',
|
|
},
|
|
],
|
|
[[\"status\", \"Status\"], settings.status, \"textarea\"],
|
|
[
|
|
[\"warning\", \"Profile warning\"],
|
|
settings.warning,
|
|
\"textarea\",
|
|
],
|
|
[[\"muted\", \"Muted phrases\"], settings.muted.join(\"\\n\"), \"textarea\", {
|
|
embed_html:
|
|
'<span class=\"fade\">Muted phrases should all be on new lines.</span>',
|
|
}],
|
|
[[], \"Accessibility\", \"title\"],
|
|
[
|
|
[\"large_text\", \"Increase UI text size\"],
|
|
\"{{ profile.settings.large_text }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[\"paged_timelines\", \"Make timelines paged instead of infinitely scrolled\"],
|
|
\"{{ profile.settings.paged_timelines }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[\"auto_clear_notifs\", \"Automatically clear all notifications when you open the notifications page\"],
|
|
\"{{ profile.settings.auto_clear_notifs }}\",
|
|
\"checkbox\",
|
|
],
|
|
],
|
|
settings,
|
|
{
|
|
muted: (new_muted) => {
|
|
settings.muted = new_muted
|
|
.split(\"\\n\")
|
|
.map((t) => t.trim());
|
|
},
|
|
},
|
|
);
|
|
|
|
ui.generate_settings_ui(
|
|
profile_settings,
|
|
[
|
|
[[], \"Privacy\", \"title\"],
|
|
[
|
|
[
|
|
\"require_account\",
|
|
\"Require an account to view my profile\",
|
|
],
|
|
\"{{ profile.settings.require_account }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[
|
|
\"private_profile\",
|
|
\"Only allow users I'm following to view my profile\",
|
|
],
|
|
\"{{ profile.settings.private_profile }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[
|
|
\"private_chats\",
|
|
\"Only allow users I'm following to add me to chats\",
|
|
],
|
|
\"{{ profile.settings.private_chats }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[
|
|
\"private_mails\",
|
|
\"Only allow users I'm following to send me mail\",
|
|
],
|
|
\"{{ profile.settings.private_mails }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[
|
|
\"private_communities\",
|
|
\"Keep my joined communities private\",
|
|
],
|
|
\"{{ profile.settings.private_communities }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[\"private_last_seen\", \"Keep my last seen time private\"],
|
|
\"{{ profile.settings.private_last_seen }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[\"hide_extra_post_tabs\", \"Hide extra post tabs (replies, media)\"],
|
|
\"{{ profile.settings.hide_extra_post_tabs }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[\"show_nsfw\", \"Show NSFW posts\"],
|
|
\"{{ profile.settings.show_nsfw }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[\"auto_unlist\", \"Automatically mark my posts as NSFW\"],
|
|
\"{{ profile.settings.auto_unlist }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[\"auto_full_unlist\", \"Only publish my posts to my profile\"],
|
|
\"{{ profile.settings.auto_full_unlist }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[],
|
|
\"Your posts will still be visible in the \\\"Following\\\" timeline for users following you.\",
|
|
\"text\",
|
|
],
|
|
[
|
|
[\"all_timeline_hide_answers\", 'Hide posts answering questions from the \"All\" timeline'],
|
|
\"{{ profile.settings.all_timeline_hide_answers }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[
|
|
\"hide_associated_blocked_users\",
|
|
\"Hide users that you've blocked on your other accounts from timelines\",
|
|
],
|
|
\"{{ profile.settings.hide_associated_blocked_users }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[
|
|
\"hide_from_social_lists\",
|
|
\"Hide my profile from social lists (followers/following)\",
|
|
],
|
|
\"{{ profile.settings.hide_from_social_lists }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[
|
|
\"hide_social_follows\",
|
|
\"Hide followers/following links on my profile\",
|
|
],
|
|
\"{{ profile.settings.hide_social_follows }}\",
|
|
\"checkbox\",
|
|
],
|
|
[[], \"Questions\", \"title\"],
|
|
[
|
|
[
|
|
\"enable_questions\",
|
|
\"Allow users to ask you questions\",
|
|
],
|
|
\"{{ profile.settings.enable_questions }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[
|
|
\"allow_anonymous_questions\",
|
|
\"Allow anonymous questions\",
|
|
],
|
|
\"{{ profile.settings.allow_anonymous_questions }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[
|
|
\"enable_drawings\",
|
|
\"Allow users to create drawings and submit them with questions\",
|
|
],
|
|
\"{{ profile.settings.enable_drawings }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[\"motivational_header\", \"Motivational header\"],
|
|
settings.motivational_header,
|
|
\"input\",
|
|
],
|
|
[[], \"Anonymous\", \"title\"],
|
|
[
|
|
[\"anonymous_username\", \"Anonymous username\"],
|
|
settings.anonymous_username,
|
|
\"input\",
|
|
],
|
|
[
|
|
[\"anonymous_avatar_url\", \"Anonymous avatar URL\"],
|
|
settings.anonymous_avatar_url,
|
|
\"input\",
|
|
],
|
|
[[], \"Signatures\", \"title\"],
|
|
[
|
|
[\"mail_signature\", \"Mail signature\"],
|
|
settings.mail_signature,
|
|
\"textarea\",
|
|
],
|
|
[
|
|
[\"forum_signature\", \"Forum signature\"],
|
|
settings.forum_signature,
|
|
\"textarea\",
|
|
],
|
|
[[], \"Economy\", \"title\"],
|
|
[
|
|
[
|
|
\"enable_shop\",
|
|
\"Show shop tab on my profile\",
|
|
],
|
|
\"{{ profile.settings.enable_shop }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[
|
|
\"no_transfers\",
|
|
\"Disable transfer requests\",
|
|
],
|
|
\"{{ profile.settings.no_transfers }}\",
|
|
\"checkbox\",
|
|
],
|
|
[[], \"Misc\", \"title\"],
|
|
[
|
|
[\"hide_dislikes\", \"Hide post dislikes\"],
|
|
\"{{ profile.settings.hide_dislikes }}\",
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[],
|
|
\"Hides dislikes on all posts. Users will also no longer be able to dislike your posts.\",
|
|
\"text\",
|
|
],
|
|
[[], \"Fun\", \"title\"],
|
|
[
|
|
[\"disable_achievements\", \"Disable achievements\"],
|
|
\"{{ profile.settings.disable_achievements }}\",
|
|
\"checkbox\",
|
|
],
|
|
],
|
|
settings,
|
|
);
|
|
|
|
const can_use_custom_css =
|
|
\"{{ user.permissions|has_supporter }}\" === \"true\";
|
|
|
|
const theme_settings_ui_json = [
|
|
[
|
|
[
|
|
\"disable_other_themes\",
|
|
\"Disable the profile theme of other users\",
|
|
],
|
|
\"{{ profile.settings.disable_other_themes }}\",
|
|
\"checkbox\",
|
|
],
|
|
[[], \"Theme builder\", \"title\"],
|
|
[
|
|
[],
|
|
\"Allow the site to build the theme for you given a base hue, saturation, and lightness. Scroll down to the next section to manually build the theme.\",
|
|
\"text\",
|
|
],
|
|
[
|
|
[\"theme_hue\", \"Theme hue (integer 0-255)\"],
|
|
\"{{ profile.settings.theme_hue }}\",
|
|
\"input\",
|
|
],
|
|
[
|
|
[\"theme_sat\", \"Theme sat (percentage 0%-100%)\"],
|
|
\"{{ profile.settings.theme_sat }}\",
|
|
\"input\",
|
|
],
|
|
[
|
|
[\"theme_lit\", \"Theme lit (percentage 0%-100%)\"],
|
|
\"{{ profile.settings.theme_lit }}\",
|
|
\"input\",
|
|
],
|
|
[[], \"Manual theme builder\", \"title\"],
|
|
[[], \"Override individual colors.\", \"text\"],
|
|
// surface
|
|
[
|
|
[\"theme_color_surface\", \"Surface\"],
|
|
\"{{ profile.settings.theme_color_surface }}\",
|
|
\"color\",
|
|
{
|
|
description: \"Page background.\",
|
|
},
|
|
],
|
|
[
|
|
[\"theme_color_text\", \"Text\"],
|
|
\"{{ profile.settings.theme_color_text }}\",
|
|
\"color\",
|
|
{
|
|
description:
|
|
\"Text on elements with the surface background.\",
|
|
},
|
|
],
|
|
[
|
|
[\"theme_color_text_link\", \"Links\"],
|
|
\"{{ profile.settings.theme_color_text_link }}\",
|
|
\"color\",
|
|
{
|
|
description: \"Links on all elements.\",
|
|
},
|
|
],
|
|
// lowered
|
|
[[], \"\", \"divider\"],
|
|
[
|
|
[\"theme_color_lowered\", \"Lowered\"],
|
|
\"{{ profile.settings.theme_color_lowered }}\",
|
|
\"color\",
|
|
{
|
|
description:
|
|
\"Some cards, buttons, or anything else with a darker background color than the surface.\",
|
|
},
|
|
],
|
|
[
|
|
[\"theme_color_text_lowered\", \"Text\"],
|
|
\"{{ profile.settings.theme_color_text_lowered }}\",
|
|
\"color\",
|
|
{
|
|
description:
|
|
\"Text on elements with the lowered backgrounds.\",
|
|
},
|
|
],
|
|
[
|
|
[\"theme_color_super_lowered\", \"Super lowered\"],
|
|
\"{{ profile.settings.theme_color_super_lowered }}\",
|
|
\"color\",
|
|
{
|
|
description: \"Borders.\",
|
|
},
|
|
],
|
|
// raised
|
|
[[], \"\", \"divider\"],
|
|
[
|
|
[\"theme_color_raised\", \"Raised\"],
|
|
\"{{ profile.settings.theme_color_raised }}\",
|
|
\"color\",
|
|
{
|
|
description:
|
|
\"Some cards, buttons, or anything else with a lighter background color than the surface.\",
|
|
},
|
|
],
|
|
[
|
|
[\"theme_color_text_raised\", \"Text\"],
|
|
\"{{ profile.settings.theme_color_text_raised }}\",
|
|
\"color\",
|
|
{
|
|
description:
|
|
\"Text on elements with the raised backgrounds.\",
|
|
},
|
|
],
|
|
[
|
|
[\"theme_color_super_raised\", \"Super raised\"],
|
|
\"{{ profile.settings.theme_color_super_raised }}\",
|
|
\"color\",
|
|
{
|
|
description: \"Some borders.\",
|
|
},
|
|
],
|
|
// primary
|
|
[[], \"\", \"divider\"],
|
|
[
|
|
[\"theme_color_primary\", \"Primary\"],
|
|
\"{{ profile.settings.theme_color_primary }}\",
|
|
\"color\",
|
|
{
|
|
description:
|
|
\"Primary color; navigation bar, some buttons, etc.\",
|
|
},
|
|
],
|
|
[
|
|
[\"theme_color_text_primary\", \"Text\"],
|
|
\"{{ profile.settings.theme_color_text_primary }}\",
|
|
\"color\",
|
|
{
|
|
description:
|
|
\"Text on elements with the primary backgrounds.\",
|
|
},
|
|
],
|
|
[
|
|
[\"theme_color_primary_lowered\", \"Lowered\"],
|
|
\"{{ profile.settings.theme_color_primary_lowered }}\",
|
|
\"color\",
|
|
{
|
|
description: \"Hover state for primary buttons.\",
|
|
},
|
|
],
|
|
// secondary
|
|
[[], \"\", \"divider\"],
|
|
[
|
|
[\"theme_color_secondary\", \"Secondary\"],
|
|
\"{{ profile.settings.theme_color_secondary }}\",
|
|
\"color\",
|
|
{
|
|
description: \"Secondary color.\",
|
|
},
|
|
],
|
|
[
|
|
[\"theme_color_text_secondary\", \"Text\"],
|
|
\"{{ profile.settings.theme_color_text_secondary }}\",
|
|
\"color\",
|
|
{
|
|
description:
|
|
\"Text on elements with the secondary backgrounds.\",
|
|
},
|
|
],
|
|
[
|
|
[\"theme_color_secondary_lowered\", \"Lowered\"],
|
|
\"{{ profile.settings.theme_color_secondary_lowered }}\",
|
|
\"color\",
|
|
{
|
|
description: \"Hover state for secondary buttons.\",
|
|
},
|
|
],
|
|
// online indicator
|
|
[[], \"\", \"divider\"],
|
|
[
|
|
[\"theme_color_online\", \"Online indicator (online)\"],
|
|
\"{{ profile.settings.theme_color_online }}\",
|
|
\"color\",
|
|
{
|
|
description:
|
|
\"The green dot next to the name of online users.\",
|
|
},
|
|
],
|
|
[
|
|
[\"theme_color_idle\", \"Online indicator (idle)\"],
|
|
\"{{ profile.settings.theme_color_idle }}\",
|
|
\"color\",
|
|
{
|
|
description:
|
|
\"The yellow dot next to the name of online users.\",
|
|
},
|
|
],
|
|
[
|
|
[\"theme_color_offline\", \"Online indicator (offline)\"],
|
|
\"{{ profile.settings.theme_color_offline }}\",
|
|
\"color\",
|
|
{
|
|
description:
|
|
\"The grey next to the name of online users.\",
|
|
},
|
|
],
|
|
];
|
|
|
|
if (can_use_custom_css) {
|
|
theme_settings_ui_json.push([[], \"Advanced\", \"title\"]);
|
|
theme_settings_ui_json.push([
|
|
[\"theme_custom_css\", \"Custom CSS\"],
|
|
settings.theme_custom_css,
|
|
\"textarea\",
|
|
{
|
|
embed_html:
|
|
'<span class=\"fade\">Custom CSS input embedded into your theme.</span>',
|
|
},
|
|
]);
|
|
}
|
|
|
|
ui.generate_settings_ui(
|
|
theme_settings,
|
|
theme_settings_ui_json,
|
|
settings,
|
|
);
|
|
|
|
globalThis.import_theme_settings = () => {
|
|
const input = document.createElement(\"input\");
|
|
input.type = \"file\";
|
|
input.accept = \"application/json\";
|
|
document.body.appendChild(input);
|
|
|
|
input.addEventListener(\"change\", async (e) => {
|
|
const json = JSON.parse(await e.target.files[0].text());
|
|
|
|
for (const setting of Object.entries(json)) {
|
|
settings[setting[0]] = setting[1];
|
|
}
|
|
|
|
input.remove();
|
|
save_settings();
|
|
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 150);
|
|
});
|
|
|
|
input.click();
|
|
};
|
|
|
|
globalThis.export_theme_settings = () => {
|
|
const theme_settings = {
|
|
profile_theme: settings.profile_theme,
|
|
};
|
|
|
|
for (const setting of Object.entries(settings)) {
|
|
if (setting[0].startsWith(\"theme_\")) {
|
|
theme_settings[setting[0]] = setting[1];
|
|
}
|
|
}
|
|
|
|
const blob = new Blob(
|
|
[JSON.stringify(theme_settings, null, 4)],
|
|
{
|
|
type: \"appliction/json\",
|
|
},
|
|
);
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const anchor = document.createElement(\"a\");
|
|
anchor.href = url;
|
|
anchor.setAttribute(\"download\", \"theme.json\");
|
|
|
|
document.body.appendChild(anchor);
|
|
anchor.click();
|
|
anchor.remove();
|
|
};
|
|
});")))
|
|
|
|
(text "{% endblock %}")
|