theming should be fine now

This commit is contained in:
Gwendolyn 2024-03-28 12:33:11 +01:00
parent 281e3495ef
commit 2548d62776
29 changed files with 1149 additions and 568 deletions

14
src/c3nav/api/settings.py Normal file
View file

@ -0,0 +1,14 @@
from django.core.handlers.wsgi import WSGIRequest
from ninja import Router as APIRouter
settings_api_router = APIRouter(tags=["settings"])
@settings_api_router.post('/theme/', auth=None, summary="set the theme for the current session")
def session_key(request: WSGIRequest, id: str | int):
if request.session.session_key is None:
request.session.create()
request.session['theme'] = int(id)
return (200,)

View file

@ -4,6 +4,7 @@ from django.views.generic.base import RedirectView
from c3nav.api.api import auth_api_router
from c3nav.api.ninja import ninja_api
from c3nav.api.settings import settings_api_router
from c3nav.editor.api.endpoints import editor_api_router
from c3nav.mapdata.api.map import map_api_router
from c3nav.mapdata.api.mapdata import mapdata_api_router
@ -21,6 +22,7 @@ ninja_api.add_router("/routing/", routing_api_router)
ninja_api.add_router("/positioning/", positioning_api_router)
ninja_api.add_router("/mapdata/", mapdata_api_router)
ninja_api.add_router("/editor/", editor_api_router)
ninja_api.add_router("/settings/", settings_api_router)
if settings.ENABLE_MESH:
from c3nav.mesh.api import mesh_api_router
ninja_api.add_router("/mesh/", mesh_api_router)

View file

@ -417,10 +417,16 @@ def create_editor_form(editor_model):
'report_help_text', 'enter_description', 'level_change_description', 'base_mapdata_accessible',
'label_settings', 'label_override', 'min_zoom', 'max_zoom', 'font_size',
'allow_levels', 'allow_spaces', 'allow_areas', 'allow_pois', 'allow_dynamic_locations',
'left', 'top', 'right', 'bottom', 'public',
'import_tag', 'import_block_data', 'import_block_geom',
'left', 'top', 'right', 'bottom', 'import_tag', 'import_block_data', 'import_block_geom',
'public', 'high_contrast', 'funky', 'randomize_primary_color', 'color_logo',
'color_css_initial', 'color_css_primary', 'color_css_secondary', 'color_css_tertiary',
'color_css_quaternary', 'color_css_quinary', 'color_css_header_background',
'color_css_header_text', 'color_css_header_text_hover',
'color_css_shadow', 'color_css_overlay_background', 'color_css_grid',
'color_css_modal_backdrop', 'color_css_route_dots_shadow', 'extra_css',
'color_background', 'color_wall_fill', 'color_wall_border', 'color_door_fill',
'color_ground_fill', 'color_obstacles_default_fill', 'color_obstacles_default_border', ]
'color_ground_fill', 'color_obstacles_default_fill', 'color_obstacles_default_border',
]
field_names = [field.name for field in editor_model._meta.get_fields() if not field.one_to_many]
existing_fields = [name for name in possible_fields if name in field_names]

View file

@ -1,32 +1,3 @@
//noinspection CssInvalidFunction
@if primary_color() != "" {
$color-primary: primary_color() !global;
$color-header-primary: primary_color() !global;
}
//noinspection CssInvalidFunction
@if header_background_color() != "" {
$color-header-background: header_background_color() !global;
}
//noinspection CssInvalidFunction
@if header_text_color() != "" {
$color-header-text: header_text_color() !global;
}
//noinspection CssInvalidFunction
@if header_text_hover_color() != "" {
$color-header-text-hover: header_text_hover_color() !global;
}
$color-initial: #fff !default
$color-primary: #9b4dca !default
$color-secondary: #606c76 !default
$color-header-background: #ffffff !default;
$color-header-primary: $color-secondary !default;
$color-header-text: $color-primary !default;
$color-header-text-hover: $color-secondary !default;
$color-test: $color-primary;
/* bootstrap overrides so it looks like the rest of the site */
body {
font-size:16px;
@ -62,40 +33,40 @@ body {
display: inline-block;
}
.navbar-default, .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus, .navbar-default .navbar-collapse {
background-color: $color-header-background;
background-color: var(--color-header-background);
}
.navbar-default .navbar-toggle {
border-color: $color-header-text;
border-color: var(--color-header-text);
}
.navbar-default .navbar-brand, .navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus {
color: $color-header-primary;
color: var(--color-primary);
}
.navbar-default .navbar-nav > li > a {
color: $color-header-text;
color: var(--color-header-text);
}
.navbar-default .navbar-nav > li > a:hover,
.navbar-default .navbar-nav > li > a:focus{
color: $color-header-text-hover;
color: var(--color-header-text-hover);
}
.navbar-collapse {
border-width: 0;
}
a, a.list-group-item, a.list-group-item:hover, a.list-group-item:focus {
color: $color-primary;
color: var(--color-primary);
}
a:hover, a:focus {
color: $color-secondary;
color: var(--color-secondary);
}
.badge {
background-color: $color-primary;
background-color: var(--color-primary);
}
.btn-primary, .btn-primary:hover, .btn-primary:focus, .btn-group.open .dropdown-toggle.btn-primary {
background-color: $color-primary;
border-color: darken($color-primary, 5%);
background-color: var(--color-primary);
border-color:color-mix(in oklab, var(--color-primary), black 5%);
}
.btn-primary:active:hover, .btn-primary.active:hover, .open > .dropdown-toggle.btn-primary:hover, .btn-primary:active:focus, .btn-primary.active:focus, .open > .dropdown-toggle.btn-primary:focus, .btn-primary:active.focus, .btn-primary.active.focus, .open > .dropdown-toggle.btn-primary.focus {
background-color: darken($color-primary, 17%);
border-color: darken($color-primary, 30%);
.btn-primary:active, .btn-primary.active, .btn-primary:active:hover, .btn-primary.active:hover, .open > .dropdown-toggle.btn-primary:hover, .btn-primary:active:focus, .btn-primary.active:focus, .open > .dropdown-toggle.btn-primary:focus, .btn-primary:active.focus, .btn-primary.active.focus, .open > .dropdown-toggle.btn-primary.focus {
background-color:color-mix(in oklab, var(--color-primary), black 17%);
border-color: color-mix(in oklab, var(--color-primary), black 30%);
}

View file

@ -13,13 +13,22 @@
{% if favicon_package %}
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'favicon_package/apple-touch-icon.png' %}">
<link rel="manifest" href="{% static 'favicon_package/site.webmanifest' %}">
<link rel="mask-icon" href="{% static 'favicon_package/safari-pinned-tab.svg' %}" color="{{ colors.safari_mask_icon_color }}">
<link rel="mask-icon" href="{% static 'favicon_package/safari-pinned-tab.svg' %}" color="{{ primary_color }}">
<meta name="apple-mobile-web-app-title" content="c3nav">
<meta name="application-name" content="c3nav">
<meta name="msapplication-TileColor" content="{{ colors.msapplication_tile_color }}">
<meta name="msapplication-TileColor" content="{{ primary_color }}">
<meta name="msapplication-config" content="{% static 'favicon_package/browserconfig.xml' %}">
{% endif %}
<meta name="theme-color" content="{{ colors.header_background_color }}">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="{{ active_theme.theme_color_light }}" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="{{ active_theme.theme_color_dark }}" />
{% if randomize_primary_color %}
<style id="c3nav-theme-randomized-primary-color">
:root {
--color-primary: {{ primary_color }}
}
</style>
{% endif %}
<style>{{ active_theme.css }}</style>
{% compress css %}
<link href="{% static 'fonts/fonts.css' %}" rel="stylesheet">
<link href="{% static 'bootstrap/css/bootstrap.css' %}" rel="stylesheet">

View file

@ -0,0 +1,118 @@
# Generated by Django 5.0.3 on 2024-03-28 15:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mapdata', '0102_rename_bssid_rangingbeacon_wifi_bssid_and_more'),
]
operations = [
migrations.AddField(
model_name='theme',
name='color_css_grid',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='CSS grid color'),
),
migrations.AddField(
model_name='theme',
name='color_css_header_background',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='CSS header background color'),
),
migrations.AddField(
model_name='theme',
name='color_css_header_text',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='CSS header text color'),
),
migrations.AddField(
model_name='theme',
name='color_css_header_text_hover',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='CSS header text hover color'),
),
migrations.AddField(
model_name='theme',
name='color_css_initial',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='CSS initial/background color'),
),
migrations.AddField(
model_name='theme',
name='color_css_modal_backdrop',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='CSS modal backdrop color'),
),
migrations.AddField(
model_name='theme',
name='color_css_overlay_background',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='CSS overlay/label background color'),
),
migrations.AddField(
model_name='theme',
name='color_css_primary',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='CSS primary/accent color'),
),
migrations.AddField(
model_name='theme',
name='color_css_quaternary',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='CSS quaternary color'),
),
migrations.AddField(
model_name='theme',
name='color_css_quinary',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='CSS quinary color'),
),
migrations.AddField(
model_name='theme',
name='color_css_route_dots_shadow',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='CSS route dots shadow color'),
),
migrations.AddField(
model_name='theme',
name='color_css_secondary',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='CSS secondary/foreground color'),
),
migrations.AddField(
model_name='theme',
name='color_css_shadow',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='CSS shadow color'),
),
migrations.AddField(
model_name='theme',
name='color_css_tertiary',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='CSS tertiary color'),
),
migrations.AddField(
model_name='theme',
name='color_logo',
field=models.TextField(blank=True, default='', verbose_name='Logo color (can be a CSS gradient if you really want it to)'),
),
migrations.AddField(
model_name='theme',
name='dark',
field=models.BooleanField(default=False, verbose_name='This is a dark theme'),
),
migrations.AddField(
model_name='theme',
name='default',
field=models.BooleanField(default=False, verbose_name='This is a default theme'),
),
migrations.AddField(
model_name='theme',
name='extra_css',
field=models.TextField(blank=True, default='', verbose_name='Extra CSS'),
),
migrations.AddField(
model_name='theme',
name='funky',
field=models.BooleanField(default=False, verbose_name='Funky (do not persist through a reload when uses chooses this theme)'),
),
migrations.AddField(
model_name='theme',
name='high_contrast',
field=models.BooleanField(default=False, verbose_name='This is a high-contrast theme'),
),
migrations.AddField(
model_name='theme',
name='randomize_primary_color',
field=models.BooleanField(default=False, verbose_name='Use random primary color'),
),
]

View file

@ -1,6 +1,7 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from c3nav import settings
from c3nav.mapdata.models import LocationGroup
from c3nav.mapdata.models.base import TitledMixin
from c3nav.mapdata.models.geometry.space import ObstacleGroup
@ -11,8 +12,39 @@ class Theme(TitledMixin, models.Model):
A theme
"""
# TODO: when a theme base colors change we need to bust the cache somehow
description = models.TextField(verbose_name=('Description'))
description = models.TextField(verbose_name=_('Description'))
public = models.BooleanField(default=False, verbose_name=_('Public'))
high_contrast = models.BooleanField(default=False, verbose_name=_('This is a high-contrast theme'))
dark = models.BooleanField(default=False, verbose_name=_('This is a dark theme'))
default = models.BooleanField(default=False, verbose_name=_('This is a default theme'))
funky = models.BooleanField(default=False, verbose_name=_(
'Funky (do not persist through a reload when uses chooses this theme)'))
randomize_primary_color = models.BooleanField(default=False, verbose_name=_('Use random primary color'))
color_logo = models.TextField(default='', blank=True,
verbose_name=_('Logo color (can be a CSS gradient if you really want it to)'))
color_css_initial = models.CharField(default='', blank=True, max_length=32, verbose_name=_('CSS initial/background color'))
color_css_primary = models.CharField(default='', blank=True, max_length=32, verbose_name=_('CSS primary/accent color'))
color_css_secondary = models.CharField(default='', blank=True, max_length=32, verbose_name=_('CSS secondary/foreground color'))
color_css_tertiary = models.CharField(default='', blank=True, max_length=32, verbose_name=_('CSS tertiary color'))
color_css_quaternary = models.CharField(default='', blank=True, max_length=32, verbose_name=_('CSS quaternary color'))
color_css_quinary = models.CharField(default='', blank=True, max_length=32, verbose_name=_('CSS quinary color'))
color_css_header_background = models.CharField(default='', blank=True, max_length=32,
verbose_name=_('CSS header background color'))
color_css_header_text = models.CharField(default='', blank=True, max_length=32, verbose_name=_('CSS header text color'))
color_css_header_text_hover = models.CharField(default='', blank=True, max_length=32,
verbose_name=_('CSS header text hover color'))
color_css_shadow = models.CharField(default='', blank=True, max_length=32, verbose_name=_('CSS shadow color'))
color_css_overlay_background = models.CharField(default='', blank=True, max_length=32,
verbose_name=_('CSS overlay/label background color'))
color_css_grid = models.CharField(default='', blank=True, max_length=32, verbose_name=_('CSS grid color'))
color_css_modal_backdrop = models.CharField(default='', blank=True, max_length=32, verbose_name=_('CSS modal backdrop color'))
color_css_route_dots_shadow = models.CharField(default='', blank=True, max_length=32,
verbose_name=_('CSS route dots shadow color'))
extra_css = models.TextField(default='', blank=True, verbose_name=_('Extra CSS'))
color_background = models.CharField(max_length=32, verbose_name=_('background color'))
color_wall_fill = models.CharField(max_length=32, verbose_name=_('wall fill color'))
color_wall_border = models.CharField(max_length=32, verbose_name=_('wall border color'))
@ -24,6 +56,26 @@ class Theme(TitledMixin, models.Model):
last_updated = models.DateTimeField(auto_now=True)
def css_vars(self):
return {
'initial': self.color_css_initial or settings.BASE_THEME['css']['initial'],
'primary': self.color_css_primary or settings.BASE_THEME['css']['primary'],
'secondary': self.color_css_secondary or settings.BASE_THEME['css']['secondary'],
'tertiary': self.color_css_tertiary or settings.BASE_THEME['css']['tertiary'],
'quaternary': self.color_css_quaternary or settings.BASE_THEME['css']['quaternary'],
'quinary': self.color_css_quinary or settings.BASE_THEME['css']['quinary'],
'header-background': self.color_css_header_background or settings.BASE_THEME['css']['header-background'],
'header-text': self.color_css_header_text or settings.BASE_THEME['css']['header-text'],
'header-text-hover': self.color_css_header_text_hover or settings.BASE_THEME['css']['header-text-hover'],
'shadow': self.color_css_shadow or settings.BASE_THEME['css']['shadow'],
'overlay-background': self.color_css_overlay_background or settings.BASE_THEME['css']['overlay-background'],
'grid': self.color_css_grid or settings.BASE_THEME['css']['grid'],
'modal-backdrop': self.color_css_modal_backdrop or settings.BASE_THEME['css']['modal-backdrop'],
'route-dots-shadow': self.color_css_route_dots_shadow or settings.BASE_THEME['css']['route-dots-shadow'],
'leaflet-background': self.color_background or settings.BASE_THEME['css']['leaflet-background'],
'logo': self.color_logo or settings.BASE_THEME['css']['logo'],
}
class Meta:
verbose_name = _('Theme')
verbose_name_plural = _('Themes')

View file

@ -1,3 +1,4 @@
from c3nav import settings
from c3nav.mapdata.models import LocationGroup
from c3nav.mapdata.models.geometry.space import ObstacleGroup
from c3nav.mapdata.models.theme import Theme
@ -15,13 +16,14 @@ class ThemeColorManager:
# TODO: border colors are not implemented yet?
def __init__(self, theme: Theme = None):
if theme is None:
self.background = RENDER_COLOR_BACKGROUND
self.wall_fill = RENDER_COLOR_WALL_FILL
self.wall_border = RENDER_COLOR_WALL_BORDER
self.door_fill = RENDER_COLOR_DOOR_FILL
self.ground_fill = RENDER_COLOR_GROUND_FILL
self.obstacles_default_fill = RENDER_COLOR_OBSTACLES_DEFAULT_FILL
self.obstacles_default_border = RENDER_COLOR_OBSTACLES_DEFAULT_BORDER
self.background = settings.BASE_THEME['map']['background']
self.wall_fill = settings.BASE_THEME['map']['wall_fill']
self.wall_border = settings.BASE_THEME['map']['wall_border']
self.door_fill = settings.BASE_THEME['map']['door_fill']
self.ground_fill = settings.BASE_THEME['map']['ground_fill']
self.obstacles_default_fill = settings.BASE_THEME['map']['obstacles_default_fill']
self.obstacles_default_border = settings.BASE_THEME['map']['obstacles_default_border']
self.highlight = settings.BASE_THEME['map']['highlight']
self.location_group_border_colors = {}
self.location_group_fill_colors = {
location_group.pk: location_group.color
@ -40,6 +42,7 @@ class ThemeColorManager:
self.ground_fill = theme.color_ground_fill
self.obstacles_default_fill = theme.color_obstacles_default_fill
self.obstacles_default_border = theme.color_obstacles_default_border
self.highlight = theme.color_css_primary
self.location_group_border_colors = {
theme_location_group.location_group_id: theme_location_group.border_color
for theme_location_group in theme.location_groups.all()

View file

@ -0,0 +1,15 @@
def mapdata_cache(func):
cache_key = None
cached_value = None
def wrapper():
nonlocal cached_value
nonlocal cache_key
from c3nav.mapdata.models import MapUpdate
current_cache_key = MapUpdate.current_cache_key()
if current_cache_key != cache_key:
cached_value = func()
cache_key = current_cache_key
return cached_value
return wrapper

View file

@ -13,7 +13,6 @@ def get_user_data(request):
'logged_in': bool(request.user.is_authenticated),
'allow_editor': can_access_editor(request),
'allow_control_panel': request.user_permissions.control_panel,
'show_nonpublic_themes': request.user_permissions.nonpublic_themes,
'has_positions': Position.user_has_positions(request.user)
}
if permissions:

View file

@ -24,9 +24,7 @@ from c3nav.mapdata.utils.cache import CachePackage, MapHistory
from c3nav.mapdata.utils.tiles import (build_access_cache_key, build_base_cache_key, build_tile_access_cookie,
build_tile_etag, get_tile_bounds, parse_tile_access_cookie)
PREVIEW_HIGHLIGHT_FILL_COLOR = settings.PRIMARY_COLOR
PREVIEW_HIGHLIGHT_FILL_OPACITY = 0.1
PREVIEW_HIGHLIGHT_STROKE_COLOR = PREVIEW_HIGHLIGHT_FILL_COLOR
PREVIEW_HIGHLIGHT_STROKE_WIDTH = 0.5
PREVIEW_IMG_WIDTH = 1200
PREVIEW_IMG_HEIGHT = 628
@ -197,10 +195,12 @@ def preview_location(request, slug):
renderer = MapRenderer(level, minx, miny, maxx, maxy, scale=img_scale, access_permissions=set())
image = renderer.render(ImageRenderEngine, theme)
if highlight:
from c3nav.mapdata.render.theme import ColorManager
color_manager = ColorManager.for_theme(theme)
for geometry in geometries:
image.add_geometry(geometry,
fill=FillAttribs(PREVIEW_HIGHLIGHT_FILL_COLOR, PREVIEW_HIGHLIGHT_FILL_OPACITY),
stroke=StrokeAttribs(PREVIEW_HIGHLIGHT_STROKE_COLOR, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
fill=FillAttribs(color_manager.highlight, PREVIEW_HIGHLIGHT_FILL_OPACITY),
stroke=StrokeAttribs(color_manager.highlight, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
category='highlight')
return image.render()
@ -302,21 +302,22 @@ def preview_route(request, slug, slug2):
def render_preview():
renderer = MapRenderer(origin_level, minx, miny, maxx, maxy, scale=img_scale, access_permissions=set())
image = renderer.render(ImageRenderEngine, theme)
from c3nav.mapdata.render.theme import ColorManager
color_manager = ColorManager.for_theme(theme)
if origin_geometry is not None:
image.add_geometry(origin_geometry,
fill=FillAttribs(PREVIEW_HIGHLIGHT_FILL_COLOR, PREVIEW_HIGHLIGHT_FILL_OPACITY),
stroke=StrokeAttribs(PREVIEW_HIGHLIGHT_STROKE_COLOR, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
fill=FillAttribs(color_manager.highlight, PREVIEW_HIGHLIGHT_FILL_OPACITY),
stroke=StrokeAttribs(color_manager.highlight, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
category='highlight')
if destination_geometry is not None:
image.add_geometry(destination_geometry,
fill=FillAttribs(PREVIEW_HIGHLIGHT_FILL_COLOR, PREVIEW_HIGHLIGHT_FILL_OPACITY),
stroke=StrokeAttribs(PREVIEW_HIGHLIGHT_STROKE_COLOR, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
fill=FillAttribs(color_manager.highlight, PREVIEW_HIGHLIGHT_FILL_OPACITY),
stroke=StrokeAttribs(color_manager.highlight, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
category='highlight')
for geom in route_geometries:
image.add_geometry(geom,
stroke=StrokeAttribs(PREVIEW_HIGHLIGHT_STROKE_COLOR, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
stroke=StrokeAttribs(color_manager.highlight, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
category='route')
return image.render()

View file

@ -11,6 +11,7 @@ import sass
from django.contrib.messages import constants as messages
from django.core.exceptions import ImproperlyConfigured
from django.utils.crypto import get_random_string
from django.utils.dateparse import parse_duration
from django.utils.translation import gettext_lazy as _
from c3nav import __version__ as c3nav_version
@ -453,8 +454,9 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.messages.context_processors.messages',
'c3nav.site.context_processors.logos',
'c3nav.site.context_processors.colors',
'c3nav.site.context_processors.user_data_json',
'c3nav.site.context_processors.theme',
'c3nav.site.context_processors.header_logo_mask',
],
'loaders': template_loaders
},
@ -491,38 +493,109 @@ COMPRESS_CSS_FILTERS = (
COMPRESS_CSS_HASHING_METHOD = 'content'
HEADER_LOGO = config.get('c3nav', 'header_logo', fallback=None)
HEADER_LOGO_MASK_MODE = config.get('c3nav', 'header_logo_mask_mode', fallback=None)
FAVICON = config.get('c3nav', 'favicon', fallback=None)
FAVICON_PACKAGE = config.get('c3nav', 'favicon_package', fallback=None)
PRIMARY_COLOR = config.get('c3nav', 'primary_color', fallback='')
HEADER_BACKGROUND_COLOR = config.get('c3nav', 'header_background_color', fallback='')
HEADER_TEXT_COLOR = config.get('c3nav', 'header_text_color', fallback='')
HEADER_TEXT_HOVER_COLOR = config.get('c3nav', 'header_text_hover_color', fallback='')
SAFARI_MASK_ICON_COLOR = config.get('c3nav', 'safari_mask_icon_color', fallback=PRIMARY_COLOR)
MSAPPLICATION_TILE_COLOR = config.get('c3nav', 'msapplication_tile_color', fallback='')
PRIMARY_COLOR_RANDOMISATION = {
'mode': config.get('primary_color_randomization', 'mode', fallback='off'),
'duration': parse_duration(config.get('primary_color_randomization', 'duration', fallback='1:00')),
'chroma': float(config.get('primary_color_randomization', 'chroma', fallback='0.5')),
'lightness': float(config.get('primary_color_randomization', 'lightness', fallback='0.3')),
}
def oklch_to_oklab(L, C, h):
from math import cos, sin
a = C * cos(h)
b = C * sin(h)
return L, a, b
def clamp(x, low, high):
return min(max(x, low), high)
def oklab_to_linear_rgb(L, a, b):
"""
see https://bottosson.github.io/posts/oklab/
"""
l_ = L + 0.3963377774 * a + 0.2158037573 * b
m_ = L - 0.1055613458 * a - 0.0638541728 * b
s_ = L - 0.0894841775 * a - 1.2914855480 * b
l = l_ * l_ * l_
m = m_ * m_ * m_
s = s_ * s_ * s_
return (
clamp(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, 0, 1),
clamp(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, 0, 1),
clamp(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s, 0, 1),
)
def linear_to_s(linear):
if linear <= 0.0031308:
return linear * 12.92
else:
return 1.055 * pow(linear, 1.0 / 2.4) - 0.055
def linear_rgb_to_srgb(r, g, b):
return linear_to_s(r), linear_to_s(g), linear_to_s(b)
def hex_from_oklch(L, C, h):
oklab = oklch_to_oklab(L, C, h)
linear_rgb = oklab_to_linear_rgb(*oklab)
srgb = linear_rgb_to_srgb(*linear_rgb)
srgb255 = tuple(int(round(x * 255, 0)) for x in srgb)
hex = '#%0.2X%0.2X%0.2X' % srgb255
return hex
RANDOM_PRIMARY_COLOR_LIST = [hex_from_oklch(PRIMARY_COLOR_RANDOMISATION['lightness'],
PRIMARY_COLOR_RANDOMISATION['chroma'],
x) for x in range(0, 360)]
BASE_THEME = {
'is_dark': config.get('theme', 'is_dark', fallback=False),
'randomize_primary_color': config.get('theme', 'randomize_primary_color', fallback=False),
'map': {
'background': config.get('theme', 'map_background', fallback='#dcdcdc'),
'wall_fill': config.get('theme', 'map_wall_fill', fallback='#aaaaaa'),
'wall_border': config.get('theme', 'map_wall_border', fallback='#666666'),
'door_fill': config.get('theme', 'map_door_fill', fallback='#ffffff'),
'ground_fill': config.get('theme', 'map_ground_fill', fallback='#eeeeee'),
'obstacles_default_fill': config.get('theme', 'map_obstacles_default_fill', fallback='#b7b7b7'),
'obstacles_default_border': config.get('theme', 'map_obstacles_default_border', fallback='#888888'),
'highlight': config.get('theme', 'css_primary', fallback='#9b4dca'),
},
'css': {
'initial': config.get('theme', 'css_initial', fallback='#ffffff'),
'primary': config.get('theme', 'css_primary', fallback='#9b4dca'),
'logo': config.get('theme', 'css_logo', fallback='#9b4dca'),
'secondary': config.get('theme', 'css_secondary', fallback='#525862'),
'tertiary': config.get('theme', 'css_tertiary', fallback='#f0f0f0'),
'quaternary': config.get('theme', 'css_quaternary', fallback='#767676'),
'quinary': config.get('theme', 'css_quinary', fallback='#cccccc'),
'header-text': config.get('theme', 'css_header_text', fallback='#ffffff'),
'header-text-hover': config.get('theme', 'css_header_text_hover', fallback='#eeeeee'),
'header-background': config.get('theme', 'css_header_background', fallback='#000000'),
'shadow': config.get('theme', 'css_shadow', fallback='#000000'),
'overlay-background': config.get('theme', 'css_overlay_background', fallback='#ffffff'),
'grid': config.get('theme', 'css_grid', fallback='#000000'),
'modal-backdrop': config.get('theme', 'css_modal_backdrop', fallback='#000000'),
'route-dots-shadow': config.get('theme', 'css_route_dots_shadow', fallback='#ffffff'),
'leaflet-background': config.get('theme', 'map_background', fallback='#dcdcdc'),
}
}
WIFI_SSIDS = [n for n in config.get('c3nav', 'wifi_ssids', fallback='').split(',') if n]
USER_REGISTRATION = config.getboolean('c3nav', 'user_registration', fallback=True)
def return_sass_color(color):
if not color:
return lambda: color
if not color.startswith('#') or len(color) != 7 or any((i not in '0123456789abcdef') for i in color[1:]):
raise ValueError('custom color is not a hex color!')
return lambda: sass.SassColor(int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16), 1)
LIBSASS_CUSTOM_FUNCTIONS = {
'primary_color': return_sass_color(PRIMARY_COLOR),
'header_background_color': return_sass_color(HEADER_BACKGROUND_COLOR),
'header_text_color': return_sass_color(HEADER_TEXT_COLOR),
'header_text_hover_color': return_sass_color(HEADER_TEXT_HOVER_COLOR),
}
INTERNAL_IPS = ('127.0.0.1', '::1')
MESSAGE_TAGS = {

View file

@ -30,14 +30,36 @@ def user_data_json(request):
}
def colors(request):
def header_logo_mask(request):
return {
'colors': {
'primary_color': settings.PRIMARY_COLOR,
'header_background_color': settings.HEADER_BACKGROUND_COLOR,
'header_text_color': settings.HEADER_TEXT_COLOR,
'header_text_hover_color': settings.HEADER_TEXT_HOVER_COLOR,
'safari_mask_icon_color': settings.SAFARI_MASK_ICON_COLOR,
'msapplication_tile_color': settings.MSAPPLICATION_TILE_COLOR,
}
'header_logo_mask_mode': settings.HEADER_LOGO_MASK_MODE,
}
def theme(request):
from c3nav.site.themes import css_themes_all, css_themes_public
if request.user_permissions.nonpublic_themes:
themes = css_themes_all()
else:
themes = css_themes_public()
active_theme_id = request.session.get('theme', 0)
if active_theme_id in themes:
active_theme = themes[active_theme_id]
else:
active_theme_id = 0
active_theme = themes[0]
request.session['theme'] = active_theme_id
if active_theme['randomize_primary_color']:
from c3nav.site.themes import get_random_primary_color
primary_color = get_random_primary_color(request)
else:
primary_color = active_theme['primary_color']
return {
'active_theme_id': active_theme_id,
'active_theme': active_theme,
'themes': themes,
'randomize_primary_color': active_theme['randomize_primary_color'],
'primary_color': primary_color,
}

File diff suppressed because it is too large Load diff

View file

@ -137,14 +137,8 @@ c3nav = {
}
const theme = localStorageWrapper.getItem('c3nav-theme');
if (theme) {
c3nav.theme = parseInt(theme);
}
c3nav.themes = JSON.parse(document.getElementById('available-themes').textContent);
if (!(c3nav.theme in c3nav.themes)) {
c3nav.theme = 0;
}
c3nav.theme = JSON.parse(document.getElementById('c3nav-active-theme').textContent);
c3nav.themes = JSON.parse(document.getElementById('c3nav-themes').textContent);
},
_searchable_locations_timer: null,
load_searchable_locations: function(firstTime) {
@ -211,8 +205,6 @@ c3nav = {
c3nav.last_site_update = JSON.parse($main.attr('data-last-site-update'));
c3nav.new_site_update = false;
c3nav._primary_color = $main.attr('data-primary-color') || L.polyline([0, 0]).options.color;
c3nav.ssids = $main.is('[data-ssids]') ? JSON.parse($main.attr('data-ssids')) : null;
c3nav.random_location_groups = $main.is('[data-random-location-groups]') ? $main.attr('data-random-location-groups').split(',').map(id => parseInt(id)) : null;
@ -503,7 +495,7 @@ c3nav = {
if (data.geometry && data.level) {
L.geoJSON(data.geometry, {
style: {
color: c3nav._primary_color,
color: 'var(--color-primary)',
fillOpacity: 0.1,
}
}).addTo(c3nav._routeLayers[data.level]);
@ -669,7 +661,7 @@ c3nav = {
var latlngs = L.GeoJSON.coordsToLatLngs(c3nav._smooth_line(coords)),
routeLayer = c3nav._routeLayers[level];
line = L.polyline(latlngs, {
color: gray ? '#888888': c3nav._primary_color,
color: gray ? '#888888': 'var(--color-primary)',
dashArray: (gray || link_to_level) ? '7' : null,
interactive: false,
smoothFactor: 0.5
@ -1247,7 +1239,7 @@ c3nav = {
$cover = $('<div>').css({
'width': width+'px',
'height': height+'px',
'background-color': '#ffffff',
'background-color': 'var(--color-background)',
'position': 'absolute',
'top': 0,
'left': $button.position().left+$button.width()/2+'px',
@ -1259,12 +1251,12 @@ c3nav = {
}, 300, 'swing');
$button.css({
'left': $button.position().left,
'background-color': '#ffffff',
'background-color': 'var(--color-background)',
'right': null,
'z-index': 201,
'opacity': 1,
'transform': 'scale(1)',
'color': c3nav._primary_color,
'color': 'var(--color-primary)',
'pointer-events': 'none'
}).animate({
left: 5,
@ -1313,7 +1305,7 @@ c3nav = {
$('#modal').toggleClass('loading', !content)
.find('#modal-content')
.html((!no_close) ? '<button class="button-clear material-symbols" id="close-modal">clear</button>' :'')
.append(content || '');
.append(content || '<div class="loader"></div>');
},
_modal_click: function(e) {
if (!c3nav.modal_noclose && (e.target.id === 'modal' || e.target.id === 'close-modal')) {
@ -1449,8 +1441,7 @@ c3nav = {
c3nav._gridLayer = new L.SquareGridLayer(JSON.parse($map.attr('data-grid')));
c3nav._gridControl = new SquareGridControl().addTo(c3nav.map);
}
if (Object.values(c3nav.themes)
.filter(([_, isPublic]) => isPublic || c3nav.user_data.show_nonpublic_themes).length > 0) {
if (Object.values(c3nav.themes).length > 1) {
new ThemeControl().addTo(c3nav.map);
}
@ -1467,24 +1458,31 @@ c3nav = {
},
theme: 0,
setTheme: function(theme) {
if (theme === c3nav.theme) return;
c3nav.theme = theme;
localStorageWrapper.setItem('c3nav-theme', c3nav.theme);
c3nav._levelControl.setTheme(c3nav.theme);
setTheme: function(id) {
if (id === c3nav.theme) return;
c3nav.theme = id;
const theme = c3nav.themes[id];
if (!theme.funky) {
c3nav_api.post('settings/theme/?id='+id);
localStorageWrapper.setItem('c3nav-theme', c3nav.theme); // TODO: instead (or additionally?) do a request to save it in the session!
}
document.querySelector('#c3nav-theme-vars').innerText = theme.css;
document.querySelector('#theme-color-meta-dark').content = theme.theme_color_dark;
document.querySelector('#theme-color-meta-light').content = theme.theme_color_light;
c3nav._levelControl.setTheme(id);
},
show_theme_select: function(e) {
e.preventDefault();
c3nav.open_modal(document.querySelector('main>.theme-selection').outerHTML);
const select = document.querySelector('#modal .theme-selection select');
for (const id in c3nav.themes) {
const [name, is_public] = c3nav.themes[id];
if (c3nav.user_data.show_nonpublic_themes || is_public) {
const option = document.createElement('option');
option.value = id;
option.innerText = name;
select.append(option);
}
for (const id of Object.keys(c3nav.themes).toSorted()) {
const theme = c3nav.themes[id];
const option = document.createElement('option');
option.value = id;
option.innerText = theme.name;
select.append(option);
}
const currentThemeOption = select.querySelector(`[value="${c3nav.theme}"]`);
if (currentThemeOption) {
@ -1492,8 +1490,8 @@ c3nav = {
}
},
select_theme: function(e) {
var theme = parseInt(e.target.parentElement.querySelector('select').value);
c3nav.setTheme(theme);
const themeId = e.target.parentElement.querySelector('select').value;
c3nav.setTheme(themeId);
history.back(); // close the modal
},
@ -1556,11 +1554,16 @@ c3nav = {
c3nav.update_map_state();
c3nav.update_location_labels();
},
_add_icon: function (name) {
c3nav[name+'Icon'] = new L.Icon({
iconUrl: '/static/img/marker-icon-'+name+'.png',
iconRetinaUrl: '/static/img/marker-icon-'+name+'-2x.png',
shadowUrl: '/static/leaflet/images/marker-shadow.png',
_add_icon: async function (name) {
var [markerSrc, shadowSrc] = await Promise.all([
fetch(`/static/img/marker.svg`).then(r => r.text()),
fetch(`/static/img/marker.svg`).then(r => r.text())
]);
c3nav[name+'Icon'] = new SvgIcon({
className: `leaflet-marker-${name}`,
iconSvg: markerSrc,
shadowSvg: shadowSrc,
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
@ -1739,7 +1742,7 @@ c3nav = {
if (data.geometry.type === "Point") return;
L.geoJSON(data.geometry, {
style: {
color: c3nav._primary_color,
color: 'var(--color-primary)',
fillOpacity: 0.2,
interactive: false,
}
@ -2378,3 +2381,62 @@ L.SquareGridLayer = L.Layer.extend({
}
}
});
var SvgIcon = L.Icon.extend({
options: {
// @section
// @aka DivIcon options
iconSize: [12, 12], // also can be set through CSS
// iconAnchor: (Point),
// popupAnchor: (Point),
// @option html: String|SVGElement = ''
// Custom HTML code to put inside the div element, empty by default. Alternatively,
// an instance of `SVGElement`.
iconSvg: null,
shadowSvg: null,
// @option bgPos: Point = [0, 0]
// Optional relative position of the background, in pixels
bgPos: null,
className: 'leaflet-svg-icon'
},
// @method createIcon(oldIcon?: HTMLElement): HTMLElement
// Called internally when the icon has to be shown, returns a `<img>` HTML element
// styled according to the options.
createIcon: function (oldIcon) {
return this._createIcon('icon', oldIcon);
},
// @method createShadow(oldIcon?: HTMLElement): HTMLElement
// As `createIcon`, but for the shadow beneath it.
createShadow: function (oldIcon) {
return this._createIcon('shadow', oldIcon);
},
_createIcon: function (name, oldIcon) {
var src = this.options[`${name}Svg`];
if (!src) {
if (name === 'icon') {
throw new Error('iconSvg not set in Icon options (see the docs).');
}
return null;
}
var svgEl;
if (src instanceof SVGElement) {
svgEl = src;
} else {
svgEl = (new DOMParser()).parseFromString(src, 'image/svg+xml').documentElement;
}
this._setIconStyles(svgEl, name);
return svgEl;
},
});

View file

@ -13,13 +13,27 @@
{% if favicon_package %}
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'favicon_package/apple-touch-icon.png' %}">
<link rel="manifest" href="{% static 'favicon_package/site.webmanifest' %}">
<link rel="mask-icon" href="{% static 'favicon_package/safari-pinned-tab.svg' %}" color="{{ colors.safari_mask_icon_color }}">
<link rel="mask-icon" href="{% static 'favicon_package/safari-pinned-tab.svg' %}" color="{{ primary_color }}">
<meta name="apple-mobile-web-app-title" content="c3nav">
<meta name="application-name" content="c3nav">
<meta name="msapplication-TileColor" content="{{ colors.msapplication_tile_color }}">
<meta name="msapplication-TileColor" content="{{ primary_color }}">
<meta name="msapplication-config" content="{% static 'favicon_package/browserconfig.xml' %}">
{% endif %}
<meta name="theme-color" content="{{ colors.header_background_color }}">
<meta name="theme-color" media="(prefers-color-scheme: light)" id="theme-color-meta-light"
content="{{ active_theme.theme_color_light }}"/>
<meta name="theme-color" media="(prefers-color-scheme: dark)" id="theme-color-meta-dark"
content="{{ active_theme.theme_color_dark }}"/>
{% if randomize_primary_color %}
<style id="c3nav-theme-randomized-primary-color">
:root {
--color-primary: {{ primary_color }};
--color-logo: {{ primary_color }};
}
</style>
{% endif %}
<style id="c3nav-theme-vars">{{ active_theme.css }}</style>
{{ themes|json_script:"c3nav-themes" }}
{{ active_theme_id|json_script:"c3nav-active-theme" }}
{% compress css %}
<link href="{% static 'fonts/fonts.css' %}" rel="stylesheet">
<link href="{% static 'normalize/normalize.css' %}" rel="stylesheet">
@ -29,21 +43,36 @@
{% endcompress %}
{% block head %}
{% endblock %}
{% if header_logo and header_logo_mask_mode %}
<style>
#header-logo-link {
mask-image: url('{% static header_logo %}');
mask-mode: {{ header_logo_mask_mode }};
mask-repeat: no-repeat;
mask-size: contain;
background: var(--color-logo);
}
#header-logo-link > img {
visibility: hidden;
}
</style>
{% endif %}
</head>
<body data-user-data="{{ user_data_json }}">
{% if not embed and not request.mobileclient %}
<header>
<h1><a href="{% block header_title_url %}/{% endblock %}">
{% if header_logo %}<img src="{% static header_logo %}">{% else %}c3nav {% endif %}{% spaceless %}
{% endspaceless %}{% block header_title %}{% endblock %}
</a></h1>
<a href="/account/" id="user">
<span>{{ request.user_data.title }}</span>
<small>{% if request.user_data.subtitle %}{{ request.user_data.subtitle }}{% endif %}</small>
</a>
</header>
{% endif %}
{% block content %}
{% endblock %}
{% if not embed and not request.mobileclient %}
<header>
<h1><a href="{% block header_title_url %}/{% endblock %}" id="header-logo-link">
{% if header_logo %}<img src="{% static header_logo %}">{% else %}c3nav {% endif %}{% spaceless %}
{% endspaceless %}{% block header_title %}{% endblock %}
</a></h1>
<a href="/account/" id="user">
<span>{{ request.user_data.title }}</span>
<small>{% if request.user_data.subtitle %}{{ request.user_data.subtitle }}{% endif %}</small>
</a>
</header>
{% endif %}
{% block content %}
{% endblock %}
</body>
</html>

View file

@ -29,12 +29,11 @@
<meta property="og:url" content="{{ meta.canonical_url }}"/>
<meta property="twitter:url" content="{{ meta.canonical_url }}"/>
{% endif %}
{{ available_themes|json_script:"available-themes" }}
{% endblock %}
{% block content %}
<main class="map" data-state="{{ state }}"{% if embed %} data-embed{% endif %} data-last-site-update="{{ last_site_update }}"{% if ssids %} data-ssids="{{ ssids }}"{% endif %} data-primary-color="{{ primary_color }}"{% if random_location_groups %} data-random-location-groups="{{ random_location_groups }}"{% endif %}>
<main class="map" data-state="{{ state }}"{% if embed %} data-embed{% endif %} data-last-site-update="{{ last_site_update }}"{% if ssids %} data-ssids="{{ ssids }}"{% endif %} {% if random_location_groups %} data-random-location-groups="{{ random_location_groups }}"{% endif %}>
<div class="loader"></div>
{% if not request.mobileclient %}
<section id="attributions">
{% if not embed %}
@ -136,6 +135,7 @@
</section>
<section id="sidebar">
<section id="search" class="loading">
<div class="loader"></div>
<div class="location locationinput empty" id="origin-input">
<i class="icon material-symbols">place</i>
<input type="text" autocomplete="off" spellcheck="false" placeholder="{% trans 'Search any location…' %}">
@ -167,6 +167,7 @@
</button>
</div>
<div id="route-summary">
<div class="loader"></div>
<i class="icon material-symbols">directions</i>
<span></span>
<small><em></em></small>
@ -196,6 +197,7 @@
<div id="resultswrapper">
<section id="autocomplete"></section>
<section id="location-details" class="details">
<div class="loader"></div>
<div class="details-head">
<button class="button close button-clear material-symbols float-right">close</button>
<h2>{% trans 'Details' %}</h2>
@ -251,7 +253,6 @@
<p>
<label for="id_theme">Theme:</label>
<select name="theme" required="" id="id_theme">
<option value="0">{% trans 'Default theme' %}</option>
</select>
</p>
<button>Save theme</button>

147
src/c3nav/site/themes.py Normal file
View file

@ -0,0 +1,147 @@
from c3nav import settings
from c3nav.mapdata.utils.cache.cache_decorator import mapdata_cache
def css_vars_as_str(vars):
css_str = ''
for name, value in vars.items():
css_str += f'--color-{name}: {value};'
return css_str
remove = ['grid']
modify = {
'grid-text': ('grid', lambda rgb: f'rgba({rgb[0]},{rgb[1]},{rgb[2]},0.6)'),
'grid-lines': ('grid', lambda rgb: f'rgba({rgb[0]},{rgb[1]},{rgb[2]},0.2)'),
'modal-backdrop': ('modal-backdrop', lambda rgb: f'rgba({rgb[0]},{rgb[1]},{rgb[2]},0.2)'),
'shadow': ('shadow', lambda rgb: f'rgba({rgb[0]},{rgb[1]},{rgb[2]},0.2)'),
'control-shadow': ('shadow', lambda rgb: f'rgba({rgb[0]},{rgb[1]},{rgb[2]},0.6)'),
'overlay-background': ('overlay-background', lambda rgb: f'rgba({rgb[0]},{rgb[1]},{rgb[2]},0.6)'),
}
def modify_vars(css_vars):
from c3nav.mapdata.utils.color import color_to_rgb
for key, (source, fn) in modify.items():
try:
rgb = [x * 255 for x in color_to_rgb(css_vars[source])]
except ValueError: # ignore invalid colors
continue
css_vars[key] = fn(rgb)
for key in remove:
del css_vars[key]
def make_themes(theme_models):
from c3nav import settings
from django.utils.translation import gettext_lazy as _
themes = {}
base_css_vars = settings.BASE_THEME['css'].copy()
modify_vars(base_css_vars)
primary_color = base_css_vars['primary']
if settings.BASE_THEME['randomize_primary_color']:
del base_css_vars['primary']
base_theme_vars_str = css_vars_as_str(base_css_vars)
base_theme = {
'css_code': ':root{%s}' % base_theme_vars_str,
'theme_color': base_css_vars['header-background'],
'randomize_primary_color': settings.BASE_THEME['randomize_primary_color'],
'primary_color': primary_color,
}
if settings.BASE_THEME['is_dark']:
default_dark = base_theme
default_light = None
else:
default_light = base_theme
default_dark = None
for theme in theme_models:
css_vars = theme.css_vars()
modify_vars(css_vars)
primary_color = css_vars['primary']
if theme.randomize_primary_color:
del css_vars['primary']
css_vars_str = css_vars_as_str(css_vars)
css_code = (':root{%s}' % css_vars_str) + theme.extra_css
themes[theme.pk] = {
'name': theme.title,
'css': css_code,
'funky': theme.funky,
'theme_color_dark': theme.color_css_header_background,
'theme_color_light': theme.color_css_header_background,
'randomize_primary_color': theme.randomize_primary_color,
'primary_color': primary_color,
}
if theme.default:
if theme.dark:
default_dark = {
'css_code': css_code,
'theme_color': css_vars['header-background'],
'primary_color': primary_color,
}
else:
default_light = {
'css_code': css_code,
'theme_color': css_vars['header-background'],
'primary_color': primary_color,
}
if default_dark is not None and default_light is not None:
name = _('Automatic')
css_code = ('@media(prefers-color-scheme:light){%s@media(prefers-color-scheme:dark){%s}'
% (default_light['css_code'], default_dark['css_code']))
randomize_primary_color = default_dark['randomize_primary_color'] or default_light['randomize_primary_color']
else:
name = _('Default')
default_theme = default_light or default_dark
css_code = default_theme['css_code']
randomize_primary_color = default_theme['randomize_primary_color']
themes[0] = {
'name': name,
'css': css_code,
'funky': False,
'theme_color_dark': default_dark['theme_color'] if default_dark is not None else default_light['theme_color'],
'theme_color_light': default_light['theme_color'] if default_light is not None else default_dark['theme_color'],
'randomize_primary_color': randomize_primary_color,
'primary_color': default_light['primary_color'] if default_light is not None else default_dark['primary_color'],
}
return themes
@mapdata_cache
def css_themes_all():
from c3nav.mapdata.models.theme import Theme
return make_themes(Theme.objects.all())
@mapdata_cache
def css_themes_public():
from c3nav.mapdata.models.theme import Theme
return make_themes(Theme.objects.filter(public=True))
def random_color():
import random
return settings.RANDOM_PRIMARY_COLOR_LIST[random.randrange(0, 360)]
def get_random_primary_color(request):
if settings.PRIMARY_COLOR_RANDOMISATION['mode'] == 'off':
return settings.BASE_THEME['css']['primary']
elif settings.PRIMARY_COLOR_RANDOMISATION['mode'] == 'request':
return random_color()
elif settings.PRIMARY_COLOR_RANDOMISATION['mode'] == 'session':
if 'randomized_primary_color' not in request.session:
request.session['randomized_primary_color'] = random_color()
return request.session['randomized_primary_color']
elif settings.PRIMARY_COLOR_RANDOMISATION['mode'] == 'time':
from django.core.cache import cache
color = cache.get('randomized_primary_color', None)
if color is None:
color = random_color()
cache.set('randomized_primary_color', color, settings.PRIMARY_COLOR_RANDOMISATION['duration'].total_seconds())
return color

View file

@ -187,7 +187,6 @@ def map_index(request, mode=None, slug=None, slug2=None, details=None, options=N
'state': json.dumps(state, separators=(',', ':'), cls=DjangoJSONEncoder),
'tile_cache_server': settings.TILE_CACHE_SERVER,
'initial_level': settings.INITIAL_LEVEL,
'primary_color': settings.PRIMARY_COLOR,
'initial_bounds': json.dumps(initial_bounds, separators=(',', ':')) if initial_bounds else None,
'last_site_update': json.dumps(SiteUpdate.last_update()),
'ssids': json.dumps(settings.WIFI_SSIDS, separators=(',', ':')) if settings.WIFI_SSIDS else None,

View file

@ -16,7 +16,7 @@ html
// Default body styles
body
color: $color-secondary
color: var(--color-secondary)
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif
font-size: 1.6em // Currently ems cause chrome bug misinterpreting rems on body element
font-weight: 300

View file

@ -3,7 +3,7 @@
//
blockquote
border-left: .3rem solid $color-quaternary
border-left: .3rem solid var(--color-quaternary)
margin-left: 0
margin-right: 0
padding: 1rem 1.5rem

View file

@ -7,10 +7,10 @@ button,
input[type='button'],
input[type='reset'],
input[type='submit']
background-color: $color-primary
border: .1rem solid $color-primary
background-color: var(--color-primary)
border: .1rem solid var(--color-primary)
border-radius: .4rem
color: $color-initial
color: var(--color-initial)
cursor: pointer
display: inline-block
font-size: 1.1rem
@ -26,9 +26,9 @@ input[type='submit']
&:focus,
&:hover
background-color: $color-secondary
border-color: $color-secondary
color: $color-initial
background-color: var(--color-secondary)
border-color: var(--color-secondary)
color: var(--color-initial)
outline: 0
&[disabled]
@ -37,39 +37,39 @@ input[type='submit']
&:focus,
&:hover
background-color: $color-primary
border-color: $color-primary
background-color: var(--color-primary)
border-color: var(--color-primary)
&.button-outline
background-color: transparent
color: $color-primary
color: var(--color-primary)
&:focus,
&:hover
background-color: transparent
border-color: $color-secondary
color: $color-secondary
border-color: var(--color-secondary)
color: var(--color-secondary)
&[disabled]
&:focus,
&:hover
border-color: inherit
color: $color-primary
color: var(--color-primary)
&.button-clear
background-color: transparent
border-color: transparent
color: $color-primary
color: var(--color-primary)
&:focus,
&:hover
background-color: transparent
border-color: transparent
color: $color-secondary
color: var(--color-secondary)
&[disabled]
&:focus,
&:hover
color: $color-primary
color: var(--color-primary)

View file

@ -3,7 +3,7 @@
//
code
background: $color-tertiary
background: var(--color-tertiary)
border-radius: .4rem
font-size: 86%
margin: 0 .2rem
@ -11,8 +11,8 @@ code
white-space: nowrap
pre
background: $color-tertiary
border-left: .3rem solid $color-primary
background: var(--color-tertiary)
border-left: .3rem solid var(--color-primary)
overflow-y: hidden
& > code

View file

@ -1,10 +0,0 @@
// Color
//
$color-initial: #fff !default
$color-primary: #9b4dca !default
$color-secondary: #606c76 !default
$color-tertiary: #f4f5f6 !default
$color-quaternary: #d1d1d1 !default
$color-quinary: #e1e1e1 !default

View file

@ -4,5 +4,5 @@
hr
border: 0
border-top: .1rem solid $color-tertiary
border-top: .1rem solid var(--color-tertiary)
margin: 3.0rem 0

View file

@ -13,7 +13,7 @@ textarea,
select
appearance: none // Removes awkward default styles on some inputs for iOS
background-color: transparent
border: .1rem solid $color-quaternary
border: .1rem solid var(--color-quaternary)
border-radius: .4rem
box-shadow: none
box-sizing: inherit // Forced to replace inherit values of the normalize.css
@ -22,7 +22,7 @@ select
width: 100%
&:focus
border-color: $color-primary
border-color: var(--color-primary)
outline: 0
select

View file

@ -3,9 +3,9 @@
//
a
color: $color-primary
color: var(--color-primary)
text-decoration: none
&:focus,
&:hover
color: $color-secondary
color: var(--color-secondary)

View file

@ -8,7 +8,7 @@ table
td,
th
border-bottom: .1rem solid $color-quinary
border-bottom: .1rem solid var(--color-quinary)
padding: 1.2rem 1.5rem
text-align: left

View file

@ -9,7 +9,6 @@
// Sass Modules
//
@import Color
@import Base
@import Blockquote
@import Button