Add dark mode support with mode query parameter
This commit is contained in:
parent
b993fe08d3
commit
fe0cb22f23
9 changed files with 161 additions and 9 deletions
|
|
@ -99,6 +99,8 @@ def load_entry_points(group_name):
|
|||
def load_jinja2_themes(config):
|
||||
"""Automatically load any entry_point registered Remarkbox theme."""
|
||||
themes = load_entry_points("remarkbox.themes")
|
||||
theme_defaults = {}
|
||||
|
||||
for theme_name, theme_module in themes.items():
|
||||
theme_module_name = theme_module.__name__
|
||||
# teach Jinja2 about the template dir in the theme package.
|
||||
|
|
@ -111,6 +113,12 @@ def load_jinja2_themes(config):
|
|||
"{}:static/theme/{}".format(theme_module_name, theme_name),
|
||||
cache_max_age=3600,
|
||||
)
|
||||
# Collect theme's default mode if defined
|
||||
if hasattr(theme_module, 'default_theme_mode'):
|
||||
theme_defaults[theme_name] = theme_module.default_theme_mode
|
||||
|
||||
# Store theme defaults in config registry for later access
|
||||
config.registry.settings['theme_defaults'] = theme_defaults
|
||||
return config
|
||||
|
||||
|
||||
|
|
@ -492,6 +500,30 @@ def main(global_config, **settings):
|
|||
def add_mathjax(request):
|
||||
return "true" if request.namespace.mathjax else "false"
|
||||
|
||||
def add_theme_mode(request):
|
||||
"""
|
||||
Return theme mode 'light' or 'dark'.
|
||||
Priority: user preference > query params > theme default > 'light'.
|
||||
"""
|
||||
# If user is authenticated and has a preference
|
||||
if request.user and request.user.authenticated and request.user.theme_mode != 'auto':
|
||||
return request.user.theme_mode
|
||||
|
||||
# Check if there's a mode parameter (for embeds or overrides)
|
||||
param_mode = request.params.get("mode")
|
||||
if param_mode in ("light", "dark"):
|
||||
return param_mode
|
||||
|
||||
# Use theme's default mode if available
|
||||
if request.theme:
|
||||
theme_defaults = request.registry.settings.get('theme_defaults', {})
|
||||
theme_default = theme_defaults.get(request.theme)
|
||||
if theme_default in ("light", "dark"):
|
||||
return theme_default
|
||||
|
||||
# Final fallback to light
|
||||
return "light"
|
||||
|
||||
# register functions to app config as request methods.
|
||||
# each request instance will run these functions and attach results.
|
||||
# cache result with `reify=True` to prevent multiple db lookups.
|
||||
|
|
@ -546,6 +578,7 @@ def main(global_config, **settings):
|
|||
config.add_request_method(add_page_offset, "page_offset", reify=True)
|
||||
config.add_request_method(add_node_order, "node_order", reify=True)
|
||||
config.add_request_method(add_mathjax, "mathjax", reify=True)
|
||||
config.add_request_method(add_theme_mode, "theme_mode", reify=True)
|
||||
|
||||
# all of the web application routes.
|
||||
config.include(".routes")
|
||||
|
|
|
|||
|
|
@ -114,6 +114,12 @@ class User(RBase, Base):
|
|||
default="daily",
|
||||
nullable=False,
|
||||
)
|
||||
# Theme mode preference: 'auto', 'light', or 'dark'. 'auto' respects parent site.
|
||||
theme_mode = Column(
|
||||
Enum('auto', 'light', 'dark', name='theme_mode_enum'),
|
||||
default='auto',
|
||||
nullable=False,
|
||||
)
|
||||
# example: cus_12345678AbCdEF but may be null.
|
||||
stripe_id = Column(Unicode(18), unique=True, nullable=True)
|
||||
|
||||
|
|
|
|||
34
remarkbox/scripts/alembic/versions/add_theme_mode_to_user.py
Normal file
34
remarkbox/scripts/alembic/versions/add_theme_mode_to_user.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
"""add theme_mode column to rb_user
|
||||
|
||||
Revision ID: b8f3c9d4e5a1
|
||||
Revises: 14a6a35940c7
|
||||
Create Date: 2025-10-11 00:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b8f3c9d4e5a1'
|
||||
down_revision = '14a6a35940c7'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column(
|
||||
'rb_user',
|
||||
sa.Column(
|
||||
'theme_mode',
|
||||
sa.Enum('auto', 'light', 'dark', name='theme_mode_enum'),
|
||||
nullable=False,
|
||||
server_default='auto',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('rb_user', 'theme_mode')
|
||||
# Also drop the enum type
|
||||
op.execute('DROP TYPE theme_mode_enum')
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
/* Variables */
|
||||
/* Variables - Light Mode (default) */
|
||||
:root {
|
||||
--color: #222222;
|
||||
--color-muted: #3f3f3f;
|
||||
|
||||
--color-code: #000000;
|
||||
|
||||
--background: #ffffff;
|
||||
--background-faint: #fafafa;
|
||||
|
||||
--primary: #d40000; /* Remarkbox logo color */
|
||||
|
|
@ -19,6 +20,28 @@
|
|||
--line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Dark Mode Variables */
|
||||
:root.dark-mode {
|
||||
--color: #e8e8e8;
|
||||
--color-muted: #b0b0b0;
|
||||
|
||||
--color-code: #e8e8e8;
|
||||
|
||||
--background: #1a1a1a;
|
||||
--background-faint: #2a2a2a;
|
||||
|
||||
--primary: #ff5555; /* Brighter red for dark mode */
|
||||
--primary-hover: #ff7777;
|
||||
--primary-inverse: #1a1a1a;
|
||||
|
||||
--border: #444444;
|
||||
--border-faint: #333333;
|
||||
|
||||
--color-mod: #ff5555;
|
||||
|
||||
--line-height: 1.6;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
line-height: var(--line-height);
|
||||
|
|
@ -26,6 +49,7 @@ body {
|
|||
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial,
|
||||
sans-serif;
|
||||
color: var(--color);
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
h1,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{%- import 'snippets/snippets.j2' as snippets with context -%}
|
||||
{% include 'snippets/owner-key-comment.j2' %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if request.theme_mode == 'dark' %}dark-mode{% endif %}">
|
||||
<head>
|
||||
<title>{% block title %}{{ request.app_domain }}{% endblock %}</title>
|
||||
<meta charset="utf-8" />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!-- Remarkbox - Your readers want to communicate with you -->
|
||||
<div id="remarkbox-div">
|
||||
<noscript>
|
||||
<iframe id=remarkbox-iframe src="{{ request.scheme }}://{{ request.host }}/embed?nojs=true" style="height:600px;width:100%;border:none!important" tabindex=0></iframe>
|
||||
<iframe id=remarkbox-iframe src="{{ request.scheme }}://{{ request.host }}/embed?nojs=true&mode={{ mode }}" style="height:600px;width:100%;border:none!important" tabindex=0></iframe>
|
||||
</noscript>
|
||||
</div>
|
||||
<script src="{{ request.scheme }}://{{ request.host }}/static/js/iframe-resizer/iframeResizer.min.js"></script>
|
||||
|
|
@ -12,10 +12,11 @@
|
|||
var thread_fragment = window.location.hash;
|
||||
|
||||
// rb owner was here.
|
||||
var rb_src = "{{ request.scheme }}://{{ request.host }}/embed" +
|
||||
var rb_src = "{{ request.scheme }}://{{ request.host }}/embed" +
|
||||
"?rb_owner_key=" + rb_owner_key +
|
||||
"&thread_title=" + encodeURI(thread_title) +
|
||||
"&thread_uri=" + encodeURIComponent(thread_uri) +
|
||||
"&thread_uri=" + encodeURIComponent(thread_uri) +
|
||||
"&mode={{ mode }}" +
|
||||
thread_fragment;
|
||||
|
||||
function create_remarkbox_iframe() {
|
||||
|
|
|
|||
|
|
@ -29,12 +29,9 @@
|
|||
|
||||
{{ snippets.user_avatar_link(request.user, 130, class="user-settings-profile-pic") }}
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<input type="checkbox" name="gravatar-checkbox" id="gravatar-checkbox" {% if request.user.gravatar %}checked{% endif %}></input> enable <a href="https://en.gravatar.com/support/what-is-gravatar/" target="_blank">gravatar</a>?
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<!--
|
||||
|
|
@ -52,6 +49,19 @@ automatically watch any threads I participate in.
|
|||
|
||||
-->
|
||||
|
||||
<br/>
|
||||
<label>Theme Mode</label>
|
||||
|
||||
<div class="theme-appearance-section">
|
||||
<input type="radio" name="theme-mode" id="theme-mode-auto" value="auto" {% if request.user.theme_mode == "auto" %}checked{% endif %} onchange="previewTheme(this.value)"></input> <label for="theme-mode-auto" class="inline-label">Auto (follow parent site)</label>
|
||||
<br/>
|
||||
<input type="radio" name="theme-mode" id="theme-mode-light" value="light" {% if request.user.theme_mode == "light" %}checked{% endif %} onchange="previewTheme(this.value)"></input> <label for="theme-mode-light" class="inline-label">Light</label>
|
||||
<br/>
|
||||
<input type="radio" name="theme-mode" id="theme-mode-dark" value="dark" {% if request.user.theme_mode == "dark" %}checked{% endif %} onchange="previewTheme(this.value)"></input> <label for="theme-mode-dark" class="inline-label">Dark</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<label>Reply Notifications</label>
|
||||
|
||||
When my comments get a reply, notify me
|
||||
|
|
@ -95,4 +105,25 @@ View and adjust your <a href="{{ request.link_prefix }}/u/watching">Namespace no
|
|||
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function previewTheme(mode) {
|
||||
const html = document.documentElement;
|
||||
|
||||
if (mode === 'dark') {
|
||||
html.classList.add('dark-mode');
|
||||
} else if (mode === 'light') {
|
||||
html.classList.remove('dark-mode');
|
||||
} else if (mode === 'auto') {
|
||||
// Auto mode: check if parent provided a mode parameter
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const parentMode = urlParams.get('mode');
|
||||
if (parentMode === 'dark') {
|
||||
html.classList.add('dark-mode');
|
||||
} else {
|
||||
html.classList.remove('dark-mode');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{%- endblock -%}
|
||||
|
|
|
|||
|
|
@ -231,6 +231,7 @@ def user_settings(request):
|
|||
default_node_watcher_frequency = request.params[
|
||||
"default-node-watcher-frequency"
|
||||
]
|
||||
theme_mode = request.params.get("theme-mode", "auto")
|
||||
|
||||
if display_name != request.user.name:
|
||||
if is_user_name_valid(display_name) == False:
|
||||
|
|
@ -324,6 +325,25 @@ def user_settings(request):
|
|||
("Invalid Enum value for reply_watcher_frequency.", "error")
|
||||
)
|
||||
|
||||
if theme_mode != request.user.theme_mode:
|
||||
if theme_mode in ('auto', 'light', 'dark'):
|
||||
request.user.theme_mode = theme_mode
|
||||
theme_labels = {
|
||||
'auto': 'Auto (follow parent site)',
|
||||
'light': 'Light',
|
||||
'dark': 'Dark'
|
||||
}
|
||||
request.session.flash(
|
||||
(
|
||||
"Theme appearance set to <b>{}</b>".format(theme_labels[theme_mode]),
|
||||
"success",
|
||||
)
|
||||
)
|
||||
else:
|
||||
request.session.flash(
|
||||
("Invalid theme mode value.", "error")
|
||||
)
|
||||
|
||||
request.dbsession.add(request.user)
|
||||
request.dbsession.flush()
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,10 @@ def show_count(request):
|
|||
@view_config(route_name="embed-iframe-min")
|
||||
def embed_iframe(request):
|
||||
"""Jinja render a text file and serve it as plain text."""
|
||||
context = {"rb_owner_key": request.params.get("rb_owner_key", "none")}
|
||||
context = {
|
||||
"rb_owner_key": request.params.get("rb_owner_key", "none"),
|
||||
"mode": request.params.get("mode", "light")
|
||||
}
|
||||
response = render_to_response("embed-iframe.txt.j2", context, request=request)
|
||||
response.content_type = "text/plain"
|
||||
if request.matched_route.name == "embed-iframe-min":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue