From 099b107c29402b408aee6584c54b0be3f7c6218a Mon Sep 17 00:00:00 2001 From: Goffi Date: Thu, 22 Oct 2026 15:44:21 +0200 Subject: [PATCH] tools (common/template): expose theme settings to the template: - `css`: additional sheets rendered after the regular css files (can be used for a dark theme) - `html_data_theme`: copied to the data-theme attribute - `themes_items`: theme selector entries (theme, label, icon) - fix missing parenthesis in `is_file` --- libervia/backend/tools/common/template.py | 65 ++++++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/libervia/backend/tools/common/template.py b/libervia/backend/tools/common/template.py index ce259b71..ddf7aca4 100644 --- a/libervia/backend/tools/common/template.py +++ b/libervia/backend/tools/common/template.py @@ -26,7 +26,7 @@ import os.path from pathlib import Path import re import time -from typing import List, Optional, Tuple, Union +from typing import List, NotRequired, Optional, Tuple, TypedDict, Union from xml.sax.saxutils import quoteattr from babel import support @@ -374,6 +374,12 @@ class Environment(jinja2.Environment): return super(Environment, self).get_template(name, parent, globals) +class ThemeItem(TypedDict): + theme: str + label: str + icon: NotRequired[str] + + class Renderer: def __init__(self, host, front_url_filter=None, trusted=False, private=False): @@ -440,7 +446,7 @@ class Renderer: "settings": {}, } theme_settings = p / "settings.json" - if theme_settings.is_file: + if theme_settings.is_file(): try: with theme_settings.open() as f: settings = json.load(f) @@ -656,6 +662,26 @@ class Renderer: except KeyError: raise exceptions.NotFound(f"no theme found for {site_name}") + def _get_themes_items(self, site: str) -> list[ThemeItem] | None: + """Build the theme selector items from the site themes. + + Get items for every theme with a "label" in its settings. + @param site: site name ("" for the default site) + @return: the items, sorted by label, None if empty + """ + themes = self.sites_themes.get(site, {}) + items: list[ThemeItem] = [] + for name, data in themes.items(): + settings = data["settings"] + if "label" not in settings: + continue + item = ThemeItem(theme=name, label=settings["label"]) + if "icon" in settings: + item["icon"] = settings["icon"] + items.append(item) + items.sort(key=lambda item: item["label"]) + return items or None + def get_static_path( self, template_data: TemplateData, filename: str, settings: Optional[dict] = None ) -> Optional[TemplateData]: @@ -1180,6 +1206,38 @@ class Renderer: if css_contents: kwargs["css_content" + suffix] = "\n".join(css_contents) + # extra css sheets from theme settings are rendered after the regular css files in + # base.html so they can override them + css_links = [] + html_data_theme = None + themes_items = None + # template_data.site is None for absolute template paths + if template_data.site is not None: + theme_settings = self.sites_themes[template_data.site][template_data.theme][ + "settings" + ] + html_data_theme = theme_settings.get("html_data_theme") + themes_items = self._get_themes_items(template_data.site) + for entry in theme_settings.get("css", []): + css_path = self.get_static_path( + template_data, entry["file"], theme_settings + ) + if css_path is None: + log.warning( + _("theme css file {file!r} not found, skipping").format( + file=entry["file"] + ) + ) + continue + link = { + "rel": "stylesheet", + "type": "text/css", + "href": self.get_front_url(css_path), + } + if entry.get("media"): + link["media"] = entry["media"] + css_links.append(link) + scripts_handler = ScriptsHandler(self, template_data) self.set_locale(locale) @@ -1190,6 +1248,9 @@ class Renderer: media_path=media_path, css_files=css_files, css_files_noscript=css_files_noscript, + css_links=css_links, + html_data_theme=html_data_theme, + themes_items=themes_items, locale=self._locale, locales=self.locales, gidx=Indexer(),