diff --git a/app/templates/_side_pane.html b/app/templates/_side_pane.html
index 73453a0f..8c0d424e 100644
--- a/app/templates/_side_pane.html
+++ b/app/templates/_side_pane.html
@@ -32,7 +32,7 @@
-
{{ community.description_html|safe if community.description_html else '' }}
+
{{ community.description_html|community_links|safe if community.description_html else '' }}
{{ community.rules_html|safe if community.rules_html else '' }}
{% if len(mods) > 0 and not community.private_mods -%}
Moderators
diff --git a/app/templates/themes/x_api/base.html b/app/templates/themes/x_api/base.html
new file mode 100644
index 00000000..80190a66
--- /dev/null
+++ b/app/templates/themes/x_api/base.html
@@ -0,0 +1,165 @@
+
+
+
+
+
+
{% if not debug_mode %}{{ g.site.name }}{% endif %}
+ {{ bootstrap.load_css() }}
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+
+ {% block app_content %}{% endblock %}
+
+
+
+
+
+ {{ bootstrap.load_js() }}
+ {% if debug_mode %}
+
+ {% endif %}
+
+
diff --git a/app/templates/themes/x_api/css/color-modes.css b/app/templates/themes/x_api/css/color-modes.css
new file mode 100644
index 00000000..9e9beed0
--- /dev/null
+++ b/app/templates/themes/x_api/css/color-modes.css
@@ -0,0 +1,29 @@
+.bi {
+ vertical-align: -.125em;
+ fill: currentColor;
+}
+
+.btn-bd-primary {
+ --bd-violet-bg: #712cf9;
+ --bd-violet-rgb: 112.520718, 44.062154, 249.437846;
+
+ --bs-btn-font-weight: 600;
+ --bs-btn-color: var(--bs-white);
+ --bs-btn-bg: var(--bd-violet-bg);
+ --bs-btn-border-color: var(--bd-violet-bg);
+ --bs-btn-hover-color: var(--bs-white);
+ --bs-btn-hover-bg: #6528e0;
+ --bs-btn-hover-border-color: #6528e0;
+ --bs-btn-focus-shadow-rgb: var(--bd-violet-rgb);
+ --bs-btn-active-color: var(--bs-btn-hover-color);
+ --bs-btn-active-bg: #5a23c8;
+ --bs-btn-active-border-color: #5a23c8;
+}
+
+.bd-mode-toggle {
+ z-index: 1500;
+}
+
+.bd-mode-toggle .dropdown-menu .active .bi {
+ display: block !important;
+}
diff --git a/app/templates/themes/x_api/css/navbars.css b/app/templates/themes/x_api/css/navbars.css
new file mode 100644
index 00000000..e717c1cd
--- /dev/null
+++ b/app/templates/themes/x_api/css/navbars.css
@@ -0,0 +1,8 @@
+body {
+ padding-bottom: 20px;
+}
+
+.navbar {
+ margin-bottom: 20px;
+}
+
diff --git a/app/templates/themes/x_api/index.html b/app/templates/themes/x_api/index.html
new file mode 100644
index 00000000..d71960d9
--- /dev/null
+++ b/app/templates/themes/x_api/index.html
@@ -0,0 +1,21 @@
+{% extends 'themes/' + theme() + '/base.html' %}
+
+{% block app_content %}
+
Site Info from API
+
+ {% if not debug_mode %}
+
(API only available in debug mode)
+ {% else %}
+
+ - version:
+ - actor_id:
+ - description:
+ - enable_downvotes:
+ - icon:
+ - name:
+ - sidebar:
+ - user_count:
+ - all languages:
+
+ {% endif %}
+{% endblock%}
diff --git a/app/templates/themes/x_api/js/color-modes.js b/app/templates/themes/x_api/js/color-modes.js
new file mode 100644
index 00000000..f5d5dbf3
--- /dev/null
+++ b/app/templates/themes/x_api/js/color-modes.js
@@ -0,0 +1,81 @@
+/*!
+ * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
+ * Copyright 2011-2024 The Bootstrap Authors
+ * Licensed under the Creative Commons Attribution 3.0 Unported License.
+ */
+
+(() => {
+ 'use strict'
+
+ const getStoredTheme = () => localStorage.getItem('theme')
+ const setStoredTheme = theme => localStorage.setItem('theme', theme)
+
+ const getPreferredTheme = () => {
+ const storedTheme = getStoredTheme()
+ if (storedTheme) {
+ return storedTheme
+ }
+
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
+ }
+
+ const setTheme = theme => {
+ if (theme === 'auto') {
+ document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
+ } else {
+ document.documentElement.setAttribute('data-bs-theme', theme)
+ }
+ }
+
+ setTheme(getPreferredTheme())
+
+ const showActiveTheme = (theme, focus = false) => {
+ const themeSwitcher = document.querySelector('#bd-theme')
+
+ if (!themeSwitcher) {
+ return
+ }
+
+ const themeSwitcherText = document.querySelector('#bd-theme-text')
+ const activeThemeIcon = document.querySelector('.theme-icon-active use')
+ const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
+ const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
+
+ document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
+ element.classList.remove('active')
+ element.setAttribute('aria-pressed', 'false')
+ })
+
+ btnToActive.classList.add('active')
+ btnToActive.setAttribute('aria-pressed', 'true')
+ activeThemeIcon.setAttribute('href', svgOfActiveBtn)
+ const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
+ themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
+
+ if (focus) {
+ themeSwitcher.focus()
+ }
+ }
+
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
+ const storedTheme = getStoredTheme()
+ if (storedTheme !== 'light' && storedTheme !== 'dark') {
+ setTheme(getPreferredTheme())
+ }
+ })
+
+ window.addEventListener('DOMContentLoaded', () => {
+ showActiveTheme(getPreferredTheme())
+
+ document.querySelectorAll('[data-bs-theme-value]')
+ .forEach(toggle => {
+ toggle.addEventListener('click', () => {
+ const theme = toggle.getAttribute('data-bs-theme-value')
+ setStoredTheme(theme)
+ setTheme(theme)
+ showActiveTheme(theme, true)
+ })
+ })
+ })
+})()
+
diff --git a/app/templates/themes/x_api/js/site.js b/app/templates/themes/x_api/js/site.js
new file mode 100644
index 00000000..7aa10161
--- /dev/null
+++ b/app/templates/themes/x_api/js/site.js
@@ -0,0 +1,30 @@
+const url = new URL(window.location.href);
+const baseUrl = `${url.protocol}//${url.host}`;
+const api = baseUrl + '/api/alpha/site'
+
+fetch(api)
+ .then(response => response.json())
+ .then(data => {
+ // navbar
+ document.querySelector('#head-title').textContent = data.site.name
+ document.querySelector('#navbar-title').innerHTML = '
' + ' ' + data.site.name
+
+ // site info
+ document.querySelector('#site_version').textContent = data.version
+ document.querySelector('#site_actor_id').textContent = data.site.actor_id
+ document.querySelector('#site_description').textContent = data.site.description
+ document.querySelector('#site_enable_downvotes').textContent = data.site.enable_downvotes
+ document.querySelector('#site_icon').textContent = data.site.icon
+ document.querySelector('#site_name').textContent = data.site.name
+ document.querySelector('#site_sidebar').textContent = data.site.sidebar
+ document.querySelector('#site_user_count').textContent = data.site.user_count
+
+ let lang_names = data.site.all_languages[0].name;
+ let lang_count = data.site.all_languages.length;
+
+ for (let i = 1; i < lang_count; i++) {
+ lang_names += ", " + data.site.all_languages[i].name;
+ }
+
+ document.querySelector('#site_all_languages').textContent = lang_names
+ })
diff --git a/app/templates/themes/x_api/svg/color-modes.svg b/app/templates/themes/x_api/svg/color-modes.svg
new file mode 100644
index 00000000..9c9a3e88
--- /dev/null
+++ b/app/templates/themes/x_api/svg/color-modes.svg
@@ -0,0 +1,15 @@
+
diff --git a/app/templates/themes/x_api/x_api.json b/app/templates/themes/x_api/x_api.json
new file mode 100644
index 00000000..eb0ad78c
--- /dev/null
+++ b/app/templates/themes/x_api/x_api.json
@@ -0,0 +1,4 @@
+{
+ "name": "X API",
+ "debug": true
+}
diff --git a/app/utils.py b/app/utils.py
index fb5a6a70..6b3bb600 100644
--- a/app/utils.py
+++ b/app/utils.py
@@ -1048,6 +1048,8 @@ def theme_list():
for dir in dirs:
if os.path.exists(f'app/templates/themes/{dir}/{dir}.json'):
theme_settings = json.loads(file_get_contents(f'app/templates/themes/{dir}/{dir}.json'))
+ if 'debug' in theme_settings and theme_settings['debug'] == True and not current_app.debug:
+ continue
result.append((dir, theme_settings['name']))
return result