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.api import auth_api_router
|
||||||
from c3nav.api.ninja import ninja_api
|
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.editor.api.endpoints import editor_api_router
|
||||||
from c3nav.mapdata.api.map import map_api_router
|
from c3nav.mapdata.api.map import map_api_router
|
||||||
from c3nav.mapdata.api.mapdata import mapdata_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("/positioning/", positioning_api_router)
|
||||||
ninja_api.add_router("/mapdata/", mapdata_api_router)
|
ninja_api.add_router("/mapdata/", mapdata_api_router)
|
||||||
ninja_api.add_router("/editor/", editor_api_router)
|
ninja_api.add_router("/editor/", editor_api_router)
|
||||||
|
ninja_api.add_router("/settings/", settings_api_router)
|
||||||
if settings.ENABLE_MESH:
|
if settings.ENABLE_MESH:
|
||||||
from c3nav.mesh.api import mesh_api_router
|
from c3nav.mesh.api import mesh_api_router
|
||||||
ninja_api.add_router("/mesh/", 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',
|
'report_help_text', 'enter_description', 'level_change_description', 'base_mapdata_accessible',
|
||||||
'label_settings', 'label_override', 'min_zoom', 'max_zoom', 'font_size',
|
'label_settings', 'label_override', 'min_zoom', 'max_zoom', 'font_size',
|
||||||
'allow_levels', 'allow_spaces', 'allow_areas', 'allow_pois', 'allow_dynamic_locations',
|
'allow_levels', 'allow_spaces', 'allow_areas', 'allow_pois', 'allow_dynamic_locations',
|
||||||
'left', 'top', 'right', 'bottom', 'public',
|
'left', 'top', 'right', 'bottom', 'import_tag', 'import_block_data', 'import_block_geom',
|
||||||
'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_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]
|
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]
|
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 */
|
/* bootstrap overrides so it looks like the rest of the site */
|
||||||
body {
|
body {
|
||||||
font-size:16px;
|
font-size:16px;
|
||||||
|
@ -62,40 +33,40 @@ body {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.navbar-default, .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus, .navbar-default .navbar-collapse {
|
.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 {
|
.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 {
|
.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 {
|
.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:hover,
|
||||||
.navbar-default .navbar-nav > li > a:focus{
|
.navbar-default .navbar-nav > li > a:focus{
|
||||||
color: $color-header-text-hover;
|
color: var(--color-header-text-hover);
|
||||||
}
|
}
|
||||||
.navbar-collapse {
|
.navbar-collapse {
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
}
|
}
|
||||||
a, a.list-group-item, a.list-group-item:hover, a.list-group-item:focus {
|
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 {
|
a:hover, a:focus {
|
||||||
color: $color-secondary;
|
color: var(--color-secondary);
|
||||||
}
|
}
|
||||||
.badge {
|
.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 {
|
.btn-primary, .btn-primary:hover, .btn-primary:focus, .btn-group.open .dropdown-toggle.btn-primary {
|
||||||
background-color: $color-primary;
|
background-color: var(--color-primary);
|
||||||
border-color: darken($color-primary, 5%);
|
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 {
|
.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: darken($color-primary, 17%);
|
background-color:color-mix(in oklab, var(--color-primary), black 17%);
|
||||||
border-color: darken($color-primary, 30%);
|
border-color: color-mix(in oklab, var(--color-primary), black 30%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,13 +13,22 @@
|
||||||
{% if favicon_package %}
|
{% if favicon_package %}
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'favicon_package/apple-touch-icon.png' %}">
|
<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="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="apple-mobile-web-app-title" content="c3nav">
|
||||||
<meta name="application-name" 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' %}">
|
<meta name="msapplication-config" content="{% static 'favicon_package/browserconfig.xml' %}">
|
||||||
{% endif %}
|
{% 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 %}
|
{% compress css %}
|
||||||
<link href="{% static 'fonts/fonts.css' %}" rel="stylesheet">
|
<link href="{% static 'fonts/fonts.css' %}" rel="stylesheet">
|
||||||
<link href="{% static 'bootstrap/css/bootstrap.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.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from c3nav import settings
|
||||||
from c3nav.mapdata.models import LocationGroup
|
from c3nav.mapdata.models import LocationGroup
|
||||||
from c3nav.mapdata.models.base import TitledMixin
|
from c3nav.mapdata.models.base import TitledMixin
|
||||||
from c3nav.mapdata.models.geometry.space import ObstacleGroup
|
from c3nav.mapdata.models.geometry.space import ObstacleGroup
|
||||||
|
@ -11,8 +12,39 @@ class Theme(TitledMixin, models.Model):
|
||||||
A theme
|
A theme
|
||||||
"""
|
"""
|
||||||
# TODO: when a theme base colors change we need to bust the cache somehow
|
# 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'))
|
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_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_fill = models.CharField(max_length=32, verbose_name=_('wall fill color'))
|
||||||
color_wall_border = models.CharField(max_length=32, verbose_name=_('wall border 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)
|
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:
|
class Meta:
|
||||||
verbose_name = _('Theme')
|
verbose_name = _('Theme')
|
||||||
verbose_name_plural = _('Themes')
|
verbose_name_plural = _('Themes')
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from c3nav import settings
|
||||||
from c3nav.mapdata.models import LocationGroup
|
from c3nav.mapdata.models import LocationGroup
|
||||||
from c3nav.mapdata.models.geometry.space import ObstacleGroup
|
from c3nav.mapdata.models.geometry.space import ObstacleGroup
|
||||||
from c3nav.mapdata.models.theme import Theme
|
from c3nav.mapdata.models.theme import Theme
|
||||||
|
@ -15,13 +16,14 @@ class ThemeColorManager:
|
||||||
# TODO: border colors are not implemented yet?
|
# TODO: border colors are not implemented yet?
|
||||||
def __init__(self, theme: Theme = None):
|
def __init__(self, theme: Theme = None):
|
||||||
if theme is None:
|
if theme is None:
|
||||||
self.background = RENDER_COLOR_BACKGROUND
|
self.background = settings.BASE_THEME['map']['background']
|
||||||
self.wall_fill = RENDER_COLOR_WALL_FILL
|
self.wall_fill = settings.BASE_THEME['map']['wall_fill']
|
||||||
self.wall_border = RENDER_COLOR_WALL_BORDER
|
self.wall_border = settings.BASE_THEME['map']['wall_border']
|
||||||
self.door_fill = RENDER_COLOR_DOOR_FILL
|
self.door_fill = settings.BASE_THEME['map']['door_fill']
|
||||||
self.ground_fill = RENDER_COLOR_GROUND_FILL
|
self.ground_fill = settings.BASE_THEME['map']['ground_fill']
|
||||||
self.obstacles_default_fill = RENDER_COLOR_OBSTACLES_DEFAULT_FILL
|
self.obstacles_default_fill = settings.BASE_THEME['map']['obstacles_default_fill']
|
||||||
self.obstacles_default_border = RENDER_COLOR_OBSTACLES_DEFAULT_BORDER
|
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_border_colors = {}
|
||||||
self.location_group_fill_colors = {
|
self.location_group_fill_colors = {
|
||||||
location_group.pk: location_group.color
|
location_group.pk: location_group.color
|
||||||
|
@ -40,6 +42,7 @@ class ThemeColorManager:
|
||||||
self.ground_fill = theme.color_ground_fill
|
self.ground_fill = theme.color_ground_fill
|
||||||
self.obstacles_default_fill = theme.color_obstacles_default_fill
|
self.obstacles_default_fill = theme.color_obstacles_default_fill
|
||||||
self.obstacles_default_border = theme.color_obstacles_default_border
|
self.obstacles_default_border = theme.color_obstacles_default_border
|
||||||
|
self.highlight = theme.color_css_primary
|
||||||
self.location_group_border_colors = {
|
self.location_group_border_colors = {
|
||||||
theme_location_group.location_group_id: theme_location_group.border_color
|
theme_location_group.location_group_id: theme_location_group.border_color
|
||||||
for theme_location_group in theme.location_groups.all()
|
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),
|
'logged_in': bool(request.user.is_authenticated),
|
||||||
'allow_editor': can_access_editor(request),
|
'allow_editor': can_access_editor(request),
|
||||||
'allow_control_panel': request.user_permissions.control_panel,
|
'allow_control_panel': request.user_permissions.control_panel,
|
||||||
'show_nonpublic_themes': request.user_permissions.nonpublic_themes,
|
|
||||||
'has_positions': Position.user_has_positions(request.user)
|
'has_positions': Position.user_has_positions(request.user)
|
||||||
}
|
}
|
||||||
if permissions:
|
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,
|
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)
|
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_FILL_OPACITY = 0.1
|
||||||
PREVIEW_HIGHLIGHT_STROKE_COLOR = PREVIEW_HIGHLIGHT_FILL_COLOR
|
|
||||||
PREVIEW_HIGHLIGHT_STROKE_WIDTH = 0.5
|
PREVIEW_HIGHLIGHT_STROKE_WIDTH = 0.5
|
||||||
PREVIEW_IMG_WIDTH = 1200
|
PREVIEW_IMG_WIDTH = 1200
|
||||||
PREVIEW_IMG_HEIGHT = 628
|
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())
|
renderer = MapRenderer(level, minx, miny, maxx, maxy, scale=img_scale, access_permissions=set())
|
||||||
image = renderer.render(ImageRenderEngine, theme)
|
image = renderer.render(ImageRenderEngine, theme)
|
||||||
if highlight:
|
if highlight:
|
||||||
|
from c3nav.mapdata.render.theme import ColorManager
|
||||||
|
color_manager = ColorManager.for_theme(theme)
|
||||||
for geometry in geometries:
|
for geometry in geometries:
|
||||||
image.add_geometry(geometry,
|
image.add_geometry(geometry,
|
||||||
fill=FillAttribs(PREVIEW_HIGHLIGHT_FILL_COLOR, PREVIEW_HIGHLIGHT_FILL_OPACITY),
|
fill=FillAttribs(color_manager.highlight, PREVIEW_HIGHLIGHT_FILL_OPACITY),
|
||||||
stroke=StrokeAttribs(PREVIEW_HIGHLIGHT_STROKE_COLOR, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
|
stroke=StrokeAttribs(color_manager.highlight, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
|
||||||
category='highlight')
|
category='highlight')
|
||||||
return image.render()
|
return image.render()
|
||||||
|
|
||||||
|
@ -302,21 +302,22 @@ def preview_route(request, slug, slug2):
|
||||||
def render_preview():
|
def render_preview():
|
||||||
renderer = MapRenderer(origin_level, minx, miny, maxx, maxy, scale=img_scale, access_permissions=set())
|
renderer = MapRenderer(origin_level, minx, miny, maxx, maxy, scale=img_scale, access_permissions=set())
|
||||||
image = renderer.render(ImageRenderEngine, theme)
|
image = renderer.render(ImageRenderEngine, theme)
|
||||||
|
from c3nav.mapdata.render.theme import ColorManager
|
||||||
|
color_manager = ColorManager.for_theme(theme)
|
||||||
if origin_geometry is not None:
|
if origin_geometry is not None:
|
||||||
image.add_geometry(origin_geometry,
|
image.add_geometry(origin_geometry,
|
||||||
fill=FillAttribs(PREVIEW_HIGHLIGHT_FILL_COLOR, PREVIEW_HIGHLIGHT_FILL_OPACITY),
|
fill=FillAttribs(color_manager.highlight, PREVIEW_HIGHLIGHT_FILL_OPACITY),
|
||||||
stroke=StrokeAttribs(PREVIEW_HIGHLIGHT_STROKE_COLOR, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
|
stroke=StrokeAttribs(color_manager.highlight, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
|
||||||
category='highlight')
|
category='highlight')
|
||||||
if destination_geometry is not None:
|
if destination_geometry is not None:
|
||||||
image.add_geometry(destination_geometry,
|
image.add_geometry(destination_geometry,
|
||||||
fill=FillAttribs(PREVIEW_HIGHLIGHT_FILL_COLOR, PREVIEW_HIGHLIGHT_FILL_OPACITY),
|
fill=FillAttribs(color_manager.highlight, PREVIEW_HIGHLIGHT_FILL_OPACITY),
|
||||||
stroke=StrokeAttribs(PREVIEW_HIGHLIGHT_STROKE_COLOR, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
|
stroke=StrokeAttribs(color_manager.highlight, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
|
||||||
category='highlight')
|
category='highlight')
|
||||||
|
|
||||||
for geom in route_geometries:
|
for geom in route_geometries:
|
||||||
image.add_geometry(geom,
|
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')
|
category='route')
|
||||||
return image.render()
|
return image.render()
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import sass
|
||||||
from django.contrib.messages import constants as messages
|
from django.contrib.messages import constants as messages
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.dateparse import parse_duration
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from c3nav import __version__ as c3nav_version
|
from c3nav import __version__ as c3nav_version
|
||||||
|
@ -453,8 +454,9 @@ TEMPLATES = [
|
||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
'c3nav.site.context_processors.logos',
|
'c3nav.site.context_processors.logos',
|
||||||
'c3nav.site.context_processors.colors',
|
|
||||||
'c3nav.site.context_processors.user_data_json',
|
'c3nav.site.context_processors.user_data_json',
|
||||||
|
'c3nav.site.context_processors.theme',
|
||||||
|
'c3nav.site.context_processors.header_logo_mask',
|
||||||
],
|
],
|
||||||
'loaders': template_loaders
|
'loaders': template_loaders
|
||||||
},
|
},
|
||||||
|
@ -491,38 +493,109 @@ COMPRESS_CSS_FILTERS = (
|
||||||
COMPRESS_CSS_HASHING_METHOD = 'content'
|
COMPRESS_CSS_HASHING_METHOD = 'content'
|
||||||
|
|
||||||
HEADER_LOGO = config.get('c3nav', 'header_logo', fallback=None)
|
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 = config.get('c3nav', 'favicon', fallback=None)
|
||||||
FAVICON_PACKAGE = config.get('c3nav', 'favicon_package', fallback=None)
|
FAVICON_PACKAGE = config.get('c3nav', 'favicon_package', fallback=None)
|
||||||
|
|
||||||
PRIMARY_COLOR = config.get('c3nav', 'primary_color', fallback='')
|
PRIMARY_COLOR_RANDOMISATION = {
|
||||||
HEADER_BACKGROUND_COLOR = config.get('c3nav', 'header_background_color', fallback='')
|
'mode': config.get('primary_color_randomization', 'mode', fallback='off'),
|
||||||
HEADER_TEXT_COLOR = config.get('c3nav', 'header_text_color', fallback='')
|
'duration': parse_duration(config.get('primary_color_randomization', 'duration', fallback='1:00')),
|
||||||
HEADER_TEXT_HOVER_COLOR = config.get('c3nav', 'header_text_hover_color', fallback='')
|
'chroma': float(config.get('primary_color_randomization', 'chroma', fallback='0.5')),
|
||||||
SAFARI_MASK_ICON_COLOR = config.get('c3nav', 'safari_mask_icon_color', fallback=PRIMARY_COLOR)
|
'lightness': float(config.get('primary_color_randomization', 'lightness', fallback='0.3')),
|
||||||
MSAPPLICATION_TILE_COLOR = config.get('c3nav', 'msapplication_tile_color', fallback='')
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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]
|
WIFI_SSIDS = [n for n in config.get('c3nav', 'wifi_ssids', fallback='').split(',') if n]
|
||||||
|
|
||||||
USER_REGISTRATION = config.getboolean('c3nav', 'user_registration', fallback=True)
|
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')
|
INTERNAL_IPS = ('127.0.0.1', '::1')
|
||||||
|
|
||||||
MESSAGE_TAGS = {
|
MESSAGE_TAGS = {
|
||||||
|
|
|
@ -30,14 +30,36 @@ def user_data_json(request):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def colors(request):
|
def header_logo_mask(request):
|
||||||
return {
|
return {
|
||||||
'colors': {
|
'header_logo_mask_mode': settings.HEADER_LOGO_MASK_MODE,
|
||||||
'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,
|
def theme(request):
|
||||||
'safari_mask_icon_color': settings.SAFARI_MASK_ICON_COLOR,
|
from c3nav.site.themes import css_themes_all, css_themes_public
|
||||||
'msapplication_tile_color': settings.MSAPPLICATION_TILE_COLOR,
|
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');
|
c3nav.theme = JSON.parse(document.getElementById('c3nav-active-theme').textContent);
|
||||||
if (theme) {
|
c3nav.themes = JSON.parse(document.getElementById('c3nav-themes').textContent);
|
||||||
c3nav.theme = parseInt(theme);
|
|
||||||
}
|
|
||||||
c3nav.themes = JSON.parse(document.getElementById('available-themes').textContent);
|
|
||||||
if (!(c3nav.theme in c3nav.themes)) {
|
|
||||||
c3nav.theme = 0;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
_searchable_locations_timer: null,
|
_searchable_locations_timer: null,
|
||||||
load_searchable_locations: function(firstTime) {
|
load_searchable_locations: function(firstTime) {
|
||||||
|
@ -211,8 +205,6 @@ c3nav = {
|
||||||
c3nav.last_site_update = JSON.parse($main.attr('data-last-site-update'));
|
c3nav.last_site_update = JSON.parse($main.attr('data-last-site-update'));
|
||||||
c3nav.new_site_update = false;
|
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.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;
|
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) {
|
if (data.geometry && data.level) {
|
||||||
L.geoJSON(data.geometry, {
|
L.geoJSON(data.geometry, {
|
||||||
style: {
|
style: {
|
||||||
color: c3nav._primary_color,
|
color: 'var(--color-primary)',
|
||||||
fillOpacity: 0.1,
|
fillOpacity: 0.1,
|
||||||
}
|
}
|
||||||
}).addTo(c3nav._routeLayers[data.level]);
|
}).addTo(c3nav._routeLayers[data.level]);
|
||||||
|
@ -669,7 +661,7 @@ c3nav = {
|
||||||
var latlngs = L.GeoJSON.coordsToLatLngs(c3nav._smooth_line(coords)),
|
var latlngs = L.GeoJSON.coordsToLatLngs(c3nav._smooth_line(coords)),
|
||||||
routeLayer = c3nav._routeLayers[level];
|
routeLayer = c3nav._routeLayers[level];
|
||||||
line = L.polyline(latlngs, {
|
line = L.polyline(latlngs, {
|
||||||
color: gray ? '#888888': c3nav._primary_color,
|
color: gray ? '#888888': 'var(--color-primary)',
|
||||||
dashArray: (gray || link_to_level) ? '7' : null,
|
dashArray: (gray || link_to_level) ? '7' : null,
|
||||||
interactive: false,
|
interactive: false,
|
||||||
smoothFactor: 0.5
|
smoothFactor: 0.5
|
||||||
|
@ -1247,7 +1239,7 @@ c3nav = {
|
||||||
$cover = $('<div>').css({
|
$cover = $('<div>').css({
|
||||||
'width': width+'px',
|
'width': width+'px',
|
||||||
'height': height+'px',
|
'height': height+'px',
|
||||||
'background-color': '#ffffff',
|
'background-color': 'var(--color-background)',
|
||||||
'position': 'absolute',
|
'position': 'absolute',
|
||||||
'top': 0,
|
'top': 0,
|
||||||
'left': $button.position().left+$button.width()/2+'px',
|
'left': $button.position().left+$button.width()/2+'px',
|
||||||
|
@ -1259,12 +1251,12 @@ c3nav = {
|
||||||
}, 300, 'swing');
|
}, 300, 'swing');
|
||||||
$button.css({
|
$button.css({
|
||||||
'left': $button.position().left,
|
'left': $button.position().left,
|
||||||
'background-color': '#ffffff',
|
'background-color': 'var(--color-background)',
|
||||||
'right': null,
|
'right': null,
|
||||||
'z-index': 201,
|
'z-index': 201,
|
||||||
'opacity': 1,
|
'opacity': 1,
|
||||||
'transform': 'scale(1)',
|
'transform': 'scale(1)',
|
||||||
'color': c3nav._primary_color,
|
'color': 'var(--color-primary)',
|
||||||
'pointer-events': 'none'
|
'pointer-events': 'none'
|
||||||
}).animate({
|
}).animate({
|
||||||
left: 5,
|
left: 5,
|
||||||
|
@ -1313,7 +1305,7 @@ c3nav = {
|
||||||
$('#modal').toggleClass('loading', !content)
|
$('#modal').toggleClass('loading', !content)
|
||||||
.find('#modal-content')
|
.find('#modal-content')
|
||||||
.html((!no_close) ? '<button class="button-clear material-symbols" id="close-modal">clear</button>' :'')
|
.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) {
|
_modal_click: function(e) {
|
||||||
if (!c3nav.modal_noclose && (e.target.id === 'modal' || e.target.id === 'close-modal')) {
|
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._gridLayer = new L.SquareGridLayer(JSON.parse($map.attr('data-grid')));
|
||||||
c3nav._gridControl = new SquareGridControl().addTo(c3nav.map);
|
c3nav._gridControl = new SquareGridControl().addTo(c3nav.map);
|
||||||
}
|
}
|
||||||
if (Object.values(c3nav.themes)
|
if (Object.values(c3nav.themes).length > 1) {
|
||||||
.filter(([_, isPublic]) => isPublic || c3nav.user_data.show_nonpublic_themes).length > 0) {
|
|
||||||
new ThemeControl().addTo(c3nav.map);
|
new ThemeControl().addTo(c3nav.map);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1467,24 +1458,31 @@ c3nav = {
|
||||||
|
|
||||||
},
|
},
|
||||||
theme: 0,
|
theme: 0,
|
||||||
setTheme: function(theme) {
|
setTheme: function(id) {
|
||||||
if (theme === c3nav.theme) return;
|
if (id === c3nav.theme) return;
|
||||||
c3nav.theme = theme;
|
c3nav.theme = id;
|
||||||
localStorageWrapper.setItem('c3nav-theme', c3nav.theme);
|
const theme = c3nav.themes[id];
|
||||||
c3nav._levelControl.setTheme(c3nav.theme);
|
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) {
|
show_theme_select: function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
c3nav.open_modal(document.querySelector('main>.theme-selection').outerHTML);
|
c3nav.open_modal(document.querySelector('main>.theme-selection').outerHTML);
|
||||||
const select = document.querySelector('#modal .theme-selection select');
|
const select = document.querySelector('#modal .theme-selection select');
|
||||||
for (const id in c3nav.themes) {
|
for (const id of Object.keys(c3nav.themes).toSorted()) {
|
||||||
const [name, is_public] = c3nav.themes[id];
|
const theme = c3nav.themes[id];
|
||||||
if (c3nav.user_data.show_nonpublic_themes || is_public) {
|
const option = document.createElement('option');
|
||||||
const option = document.createElement('option');
|
option.value = id;
|
||||||
option.value = id;
|
option.innerText = theme.name;
|
||||||
option.innerText = name;
|
select.append(option);
|
||||||
select.append(option);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const currentThemeOption = select.querySelector(`[value="${c3nav.theme}"]`);
|
const currentThemeOption = select.querySelector(`[value="${c3nav.theme}"]`);
|
||||||
if (currentThemeOption) {
|
if (currentThemeOption) {
|
||||||
|
@ -1492,8 +1490,8 @@ c3nav = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
select_theme: function(e) {
|
select_theme: function(e) {
|
||||||
var theme = parseInt(e.target.parentElement.querySelector('select').value);
|
const themeId = e.target.parentElement.querySelector('select').value;
|
||||||
c3nav.setTheme(theme);
|
c3nav.setTheme(themeId);
|
||||||
history.back(); // close the modal
|
history.back(); // close the modal
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1556,11 +1554,16 @@ c3nav = {
|
||||||
c3nav.update_map_state();
|
c3nav.update_map_state();
|
||||||
c3nav.update_location_labels();
|
c3nav.update_location_labels();
|
||||||
},
|
},
|
||||||
_add_icon: function (name) {
|
_add_icon: async function (name) {
|
||||||
c3nav[name+'Icon'] = new L.Icon({
|
var [markerSrc, shadowSrc] = await Promise.all([
|
||||||
iconUrl: '/static/img/marker-icon-'+name+'.png',
|
fetch(`/static/img/marker.svg`).then(r => r.text()),
|
||||||
iconRetinaUrl: '/static/img/marker-icon-'+name+'-2x.png',
|
fetch(`/static/img/marker.svg`).then(r => r.text())
|
||||||
shadowUrl: '/static/leaflet/images/marker-shadow.png',
|
]);
|
||||||
|
|
||||||
|
c3nav[name+'Icon'] = new SvgIcon({
|
||||||
|
className: `leaflet-marker-${name}`,
|
||||||
|
iconSvg: markerSrc,
|
||||||
|
shadowSvg: shadowSrc,
|
||||||
iconSize: [25, 41],
|
iconSize: [25, 41],
|
||||||
iconAnchor: [12, 41],
|
iconAnchor: [12, 41],
|
||||||
popupAnchor: [1, -34],
|
popupAnchor: [1, -34],
|
||||||
|
@ -1739,7 +1742,7 @@ c3nav = {
|
||||||
if (data.geometry.type === "Point") return;
|
if (data.geometry.type === "Point") return;
|
||||||
L.geoJSON(data.geometry, {
|
L.geoJSON(data.geometry, {
|
||||||
style: {
|
style: {
|
||||||
color: c3nav._primary_color,
|
color: 'var(--color-primary)',
|
||||||
fillOpacity: 0.2,
|
fillOpacity: 0.2,
|
||||||
interactive: false,
|
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 %}
|
{% if favicon_package %}
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'favicon_package/apple-touch-icon.png' %}">
|
<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="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="apple-mobile-web-app-title" content="c3nav">
|
||||||
<meta name="application-name" 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' %}">
|
<meta name="msapplication-config" content="{% static 'favicon_package/browserconfig.xml' %}">
|
||||||
{% endif %}
|
{% 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 %}
|
{% compress css %}
|
||||||
<link href="{% static 'fonts/fonts.css' %}" rel="stylesheet">
|
<link href="{% static 'fonts/fonts.css' %}" rel="stylesheet">
|
||||||
<link href="{% static 'normalize/normalize.css' %}" rel="stylesheet">
|
<link href="{% static 'normalize/normalize.css' %}" rel="stylesheet">
|
||||||
|
@ -29,21 +43,36 @@
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% endblock %}
|
{% 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>
|
</head>
|
||||||
<body data-user-data="{{ user_data_json }}">
|
<body data-user-data="{{ user_data_json }}">
|
||||||
{% if not embed and not request.mobileclient %}
|
{% if not embed and not request.mobileclient %}
|
||||||
<header>
|
<header>
|
||||||
<h1><a href="{% block header_title_url %}/{% endblock %}">
|
<h1><a href="{% block header_title_url %}/{% endblock %}" id="header-logo-link">
|
||||||
{% if header_logo %}<img src="{% static header_logo %}">{% else %}c3nav {% endif %}{% spaceless %}
|
{% if header_logo %}<img src="{% static header_logo %}">{% else %}c3nav {% endif %}{% spaceless %}
|
||||||
{% endspaceless %}{% block header_title %}{% endblock %}
|
{% endspaceless %}{% block header_title %}{% endblock %}
|
||||||
</a></h1>
|
</a></h1>
|
||||||
<a href="/account/" id="user">
|
<a href="/account/" id="user">
|
||||||
<span>{{ request.user_data.title }}</span>
|
<span>{{ request.user_data.title }}</span>
|
||||||
<small>{% if request.user_data.subtitle %}{{ request.user_data.subtitle }}{% endif %}</small>
|
<small>{% if request.user_data.subtitle %}{{ request.user_data.subtitle }}{% endif %}</small>
|
||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -29,12 +29,11 @@
|
||||||
<meta property="og:url" content="{{ meta.canonical_url }}"/>
|
<meta property="og:url" content="{{ meta.canonical_url }}"/>
|
||||||
<meta property="twitter:url" content="{{ meta.canonical_url }}"/>
|
<meta property="twitter:url" content="{{ meta.canonical_url }}"/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ available_themes|json_script:"available-themes" }}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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 %}
|
{% if not request.mobileclient %}
|
||||||
<section id="attributions">
|
<section id="attributions">
|
||||||
{% if not embed %}
|
{% if not embed %}
|
||||||
|
@ -136,6 +135,7 @@
|
||||||
</section>
|
</section>
|
||||||
<section id="sidebar">
|
<section id="sidebar">
|
||||||
<section id="search" class="loading">
|
<section id="search" class="loading">
|
||||||
|
<div class="loader"></div>
|
||||||
<div class="location locationinput empty" id="origin-input">
|
<div class="location locationinput empty" id="origin-input">
|
||||||
<i class="icon material-symbols">place</i>
|
<i class="icon material-symbols">place</i>
|
||||||
<input type="text" autocomplete="off" spellcheck="false" placeholder="{% trans 'Search any location…' %}">
|
<input type="text" autocomplete="off" spellcheck="false" placeholder="{% trans 'Search any location…' %}">
|
||||||
|
@ -167,6 +167,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="route-summary">
|
<div id="route-summary">
|
||||||
|
<div class="loader"></div>
|
||||||
<i class="icon material-symbols">directions</i>
|
<i class="icon material-symbols">directions</i>
|
||||||
<span></span>
|
<span></span>
|
||||||
<small><em></em></small>
|
<small><em></em></small>
|
||||||
|
@ -196,6 +197,7 @@
|
||||||
<div id="resultswrapper">
|
<div id="resultswrapper">
|
||||||
<section id="autocomplete"></section>
|
<section id="autocomplete"></section>
|
||||||
<section id="location-details" class="details">
|
<section id="location-details" class="details">
|
||||||
|
<div class="loader"></div>
|
||||||
<div class="details-head">
|
<div class="details-head">
|
||||||
<button class="button close button-clear material-symbols float-right">close</button>
|
<button class="button close button-clear material-symbols float-right">close</button>
|
||||||
<h2>{% trans 'Details' %}</h2>
|
<h2>{% trans 'Details' %}</h2>
|
||||||
|
@ -251,7 +253,6 @@
|
||||||
<p>
|
<p>
|
||||||
<label for="id_theme">Theme:</label>
|
<label for="id_theme">Theme:</label>
|
||||||
<select name="theme" required="" id="id_theme">
|
<select name="theme" required="" id="id_theme">
|
||||||
<option value="0">{% trans 'Default theme' %}</option>
|
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
<button>Save theme</button>
|
<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),
|
'state': json.dumps(state, separators=(',', ':'), cls=DjangoJSONEncoder),
|
||||||
'tile_cache_server': settings.TILE_CACHE_SERVER,
|
'tile_cache_server': settings.TILE_CACHE_SERVER,
|
||||||
'initial_level': settings.INITIAL_LEVEL,
|
'initial_level': settings.INITIAL_LEVEL,
|
||||||
'primary_color': settings.PRIMARY_COLOR,
|
|
||||||
'initial_bounds': json.dumps(initial_bounds, separators=(',', ':')) if initial_bounds else None,
|
'initial_bounds': json.dumps(initial_bounds, separators=(',', ':')) if initial_bounds else None,
|
||||||
'last_site_update': json.dumps(SiteUpdate.last_update()),
|
'last_site_update': json.dumps(SiteUpdate.last_update()),
|
||||||
'ssids': json.dumps(settings.WIFI_SSIDS, separators=(',', ':')) if settings.WIFI_SSIDS else None,
|
'ssids': json.dumps(settings.WIFI_SSIDS, separators=(',', ':')) if settings.WIFI_SSIDS else None,
|
||||||
|
|
|
@ -16,7 +16,7 @@ html
|
||||||
|
|
||||||
// Default body styles
|
// Default body styles
|
||||||
body
|
body
|
||||||
color: $color-secondary
|
color: var(--color-secondary)
|
||||||
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif
|
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif
|
||||||
font-size: 1.6em // Currently ems cause chrome bug misinterpreting rems on body element
|
font-size: 1.6em // Currently ems cause chrome bug misinterpreting rems on body element
|
||||||
font-weight: 300
|
font-weight: 300
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
// ––––––––––––––––––––––––––––––––––––––––––––––––––
|
// ––––––––––––––––––––––––––––––––––––––––––––––––––
|
||||||
|
|
||||||
blockquote
|
blockquote
|
||||||
border-left: .3rem solid $color-quaternary
|
border-left: .3rem solid var(--color-quaternary)
|
||||||
margin-left: 0
|
margin-left: 0
|
||||||
margin-right: 0
|
margin-right: 0
|
||||||
padding: 1rem 1.5rem
|
padding: 1rem 1.5rem
|
||||||
|
|
|
@ -7,10 +7,10 @@ button,
|
||||||
input[type='button'],
|
input[type='button'],
|
||||||
input[type='reset'],
|
input[type='reset'],
|
||||||
input[type='submit']
|
input[type='submit']
|
||||||
background-color: $color-primary
|
background-color: var(--color-primary)
|
||||||
border: .1rem solid $color-primary
|
border: .1rem solid var(--color-primary)
|
||||||
border-radius: .4rem
|
border-radius: .4rem
|
||||||
color: $color-initial
|
color: var(--color-initial)
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
display: inline-block
|
display: inline-block
|
||||||
font-size: 1.1rem
|
font-size: 1.1rem
|
||||||
|
@ -26,9 +26,9 @@ input[type='submit']
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover
|
&:hover
|
||||||
background-color: $color-secondary
|
background-color: var(--color-secondary)
|
||||||
border-color: $color-secondary
|
border-color: var(--color-secondary)
|
||||||
color: $color-initial
|
color: var(--color-initial)
|
||||||
outline: 0
|
outline: 0
|
||||||
|
|
||||||
&[disabled]
|
&[disabled]
|
||||||
|
@ -37,39 +37,39 @@ input[type='submit']
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover
|
&:hover
|
||||||
background-color: $color-primary
|
background-color: var(--color-primary)
|
||||||
border-color: $color-primary
|
border-color: var(--color-primary)
|
||||||
|
|
||||||
&.button-outline
|
&.button-outline
|
||||||
background-color: transparent
|
background-color: transparent
|
||||||
color: $color-primary
|
color: var(--color-primary)
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover
|
&:hover
|
||||||
background-color: transparent
|
background-color: transparent
|
||||||
border-color: $color-secondary
|
border-color: var(--color-secondary)
|
||||||
color: $color-secondary
|
color: var(--color-secondary)
|
||||||
|
|
||||||
&[disabled]
|
&[disabled]
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover
|
&:hover
|
||||||
border-color: inherit
|
border-color: inherit
|
||||||
color: $color-primary
|
color: var(--color-primary)
|
||||||
|
|
||||||
&.button-clear
|
&.button-clear
|
||||||
background-color: transparent
|
background-color: transparent
|
||||||
border-color: transparent
|
border-color: transparent
|
||||||
color: $color-primary
|
color: var(--color-primary)
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover
|
&:hover
|
||||||
background-color: transparent
|
background-color: transparent
|
||||||
border-color: transparent
|
border-color: transparent
|
||||||
color: $color-secondary
|
color: var(--color-secondary)
|
||||||
|
|
||||||
&[disabled]
|
&[disabled]
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover
|
&:hover
|
||||||
color: $color-primary
|
color: var(--color-primary)
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
// ––––––––––––––––––––––––––––––––––––––––––––––––––
|
// ––––––––––––––––––––––––––––––––––––––––––––––––––
|
||||||
|
|
||||||
code
|
code
|
||||||
background: $color-tertiary
|
background: var(--color-tertiary)
|
||||||
border-radius: .4rem
|
border-radius: .4rem
|
||||||
font-size: 86%
|
font-size: 86%
|
||||||
margin: 0 .2rem
|
margin: 0 .2rem
|
||||||
|
@ -11,8 +11,8 @@ code
|
||||||
white-space: nowrap
|
white-space: nowrap
|
||||||
|
|
||||||
pre
|
pre
|
||||||
background: $color-tertiary
|
background: var(--color-tertiary)
|
||||||
border-left: .3rem solid $color-primary
|
border-left: .3rem solid var(--color-primary)
|
||||||
overflow-y: hidden
|
overflow-y: hidden
|
||||||
|
|
||||||
& > code
|
& > 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
|
hr
|
||||||
border: 0
|
border: 0
|
||||||
border-top: .1rem solid $color-tertiary
|
border-top: .1rem solid var(--color-tertiary)
|
||||||
margin: 3.0rem 0
|
margin: 3.0rem 0
|
||||||
|
|
|
@ -13,7 +13,7 @@ textarea,
|
||||||
select
|
select
|
||||||
appearance: none // Removes awkward default styles on some inputs for iOS
|
appearance: none // Removes awkward default styles on some inputs for iOS
|
||||||
background-color: transparent
|
background-color: transparent
|
||||||
border: .1rem solid $color-quaternary
|
border: .1rem solid var(--color-quaternary)
|
||||||
border-radius: .4rem
|
border-radius: .4rem
|
||||||
box-shadow: none
|
box-shadow: none
|
||||||
box-sizing: inherit // Forced to replace inherit values of the normalize.css
|
box-sizing: inherit // Forced to replace inherit values of the normalize.css
|
||||||
|
@ -22,7 +22,7 @@ select
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
&:focus
|
&:focus
|
||||||
border-color: $color-primary
|
border-color: var(--color-primary)
|
||||||
outline: 0
|
outline: 0
|
||||||
|
|
||||||
select
|
select
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
// ––––––––––––––––––––––––––––––––––––––––––––––––––
|
// ––––––––––––––––––––––––––––––––––––––––––––––––––
|
||||||
|
|
||||||
a
|
a
|
||||||
color: $color-primary
|
color: var(--color-primary)
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover
|
&:hover
|
||||||
color: $color-secondary
|
color: var(--color-secondary)
|
||||||
|
|
|
@ -8,7 +8,7 @@ table
|
||||||
|
|
||||||
td,
|
td,
|
||||||
th
|
th
|
||||||
border-bottom: .1rem solid $color-quinary
|
border-bottom: .1rem solid var(--color-quinary)
|
||||||
padding: 1.2rem 1.5rem
|
padding: 1.2rem 1.5rem
|
||||||
text-align: left
|
text-align: left
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
// Sass Modules
|
// Sass Modules
|
||||||
// ––––––––––––––––––––––––––––––––––––––––––––––––––
|
// ––––––––––––––––––––––––––––––––––––––––––––––––––
|
||||||
|
|
||||||
@import Color
|
|
||||||
@import Base
|
@import Base
|
||||||
@import Blockquote
|
@import Blockquote
|
||||||
@import Button
|
@import Button
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue