theming should be fine now
This commit is contained in:
parent
281e3495ef
commit
2548d62776
29 changed files with 1149 additions and 568 deletions
14
src/c3nav/api/settings.py
Normal file
14
src/c3nav/api/settings.py
Normal 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,)
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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%);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
15
src/c3nav/mapdata/utils/cache/cache_decorator.py
vendored
Normal file
15
src/c3nav/mapdata/utils/cache/cache_decorator.py
vendored
Normal 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
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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
147
src/c3nav/site/themes.py
Normal 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
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -4,5 +4,5 @@
|
|||
|
||||
hr
|
||||
border: 0
|
||||
border-top: .1rem solid $color-tertiary
|
||||
border-top: .1rem solid var(--color-tertiary)
|
||||
margin: 3.0rem 0
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
// ––––––––––––––––––––––––––––––––––––––––––––––––––
|
||||
|
||||
a
|
||||
color: $color-primary
|
||||
color: var(--color-primary)
|
||||
text-decoration: none
|
||||
|
||||
&:focus,
|
||||
&:hover
|
||||
color: $color-secondary
|
||||
color: var(--color-secondary)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
// Sass Modules
|
||||
// ––––––––––––––––––––––––––––––––––––––––––––––––––
|
||||
|
||||
@import Color
|
||||
@import Base
|
||||
@import Blockquote
|
||||
@import Button
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue