Compare commits
	
		
			31 Commits
		
	
	
		
			7cdf44b901
			...
			eb5bdcf167
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| eb5bdcf167 | |||
| c74ec6fb16 | |||
| c79f040fa1 | |||
| 0ba0f8ae72 | |||
| afc2e41bdb | |||
| bba4935791 | |||
| dab4146735 | |||
| 1aababe2ae | |||
| 0dd961e0bb | |||
| f9da318a38 | |||
| 26fa426416 | |||
| 71f4d2473e | |||
| 2e9ebaded2 | |||
| c6a4196319 | |||
| be224d0efb | |||
| 89f80ad103 | |||
| 6d556fcd5d | |||
| 4201d50f4b | |||
| 6c942f36fa | |||
| d51b9ee58b | |||
| 1a27ae2a76 | |||
| e5862ee59b | |||
| 8f88b15536 | |||
| 10dfe3811b | |||
| f71c201020 | |||
| 0812f3a0a1 | |||
| ad2ed17c34 | |||
| 9db69580e0 | |||
| 4ead6b154b | |||
| 811cc97e07 | |||
| b794e24d0c | 
@ -1,14 +1,23 @@
 | 
			
		||||
import django_filters as filters
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from .models import Episode, Page
 | 
			
		||||
from . import models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = (
 | 
			
		||||
    "PageFilters",
 | 
			
		||||
    "EpisodeFilters",
 | 
			
		||||
    "ImageFilterSet",
 | 
			
		||||
    "SoundFilterSet",
 | 
			
		||||
    "TrackFilterSet",
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PageFilters(filters.FilterSet):
 | 
			
		||||
    q = filters.CharFilter(method="search_filter", label=_("Search"))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Page
 | 
			
		||||
        model = models.Page
 | 
			
		||||
        fields = {
 | 
			
		||||
            "category__id": ["in", "exact"],
 | 
			
		||||
            "pub_date": ["exact", "gte", "lte"],
 | 
			
		||||
@ -22,10 +31,33 @@ class EpisodeFilters(PageFilters):
 | 
			
		||||
    podcast = filters.BooleanFilter(method="podcast_filter", label=_("Podcast"))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Episode
 | 
			
		||||
        model = models.Episode
 | 
			
		||||
        fields = PageFilters.Meta.fields.copy()
 | 
			
		||||
 | 
			
		||||
    def podcast_filter(self, queryset, name, value):
 | 
			
		||||
        if value:
 | 
			
		||||
            return queryset.filter(sound__is_public=True).distinct()
 | 
			
		||||
        return queryset.filter(sound__isnull=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ImageFilterSet(filters.FilterSet):
 | 
			
		||||
    search = filters.CharFilter(field_name="search", method="search_filter")
 | 
			
		||||
 | 
			
		||||
    def search_filter(self, queryset, name, value):
 | 
			
		||||
        return queryset.filter(original_filename__icontains=value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SoundFilterSet(filters.FilterSet):
 | 
			
		||||
    station = filters.NumberFilter(field_name="program__station__id")
 | 
			
		||||
    program = filters.NumberFilter(field_name="program_id")
 | 
			
		||||
    episode = filters.NumberFilter(field_name="episode_id")
 | 
			
		||||
    search = filters.CharFilter(field_name="search", method="search_filter")
 | 
			
		||||
 | 
			
		||||
    def search_filter(self, queryset, name, value):
 | 
			
		||||
        return queryset.search(value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TrackFilterSet(filters.FilterSet):
 | 
			
		||||
    artist = filters.CharFilter(field_name="artist", lookup_expr="icontains")
 | 
			
		||||
    album = filters.CharFilter(field_name="album", lookup_expr="icontains")
 | 
			
		||||
    title = filters.CharFilter(field_name="title", lookup_expr="icontains")
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,15 @@
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.forms import ModelForm, ImageField, FileField
 | 
			
		||||
 | 
			
		||||
from ckeditor.fields import RichTextField
 | 
			
		||||
from filer.models.imagemodels import Image
 | 
			
		||||
from filer.models.filemodels import File
 | 
			
		||||
 | 
			
		||||
from aircox.models import Comment, Episode, Program
 | 
			
		||||
from aircox import models
 | 
			
		||||
from aircox.controllers.sound_file import SoundFile
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CommentForm(ModelForm):
 | 
			
		||||
__all__ = ("CommentForm", "PageForm", "ProgramForm", "EpisodeForm")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CommentForm(forms.ModelForm):
 | 
			
		||||
    nickname = forms.CharField()
 | 
			
		||||
    email = forms.EmailField(required=False)
 | 
			
		||||
    content = forms.CharField(widget=forms.Textarea())
 | 
			
		||||
@ -19,32 +19,31 @@ class CommentForm(ModelForm):
 | 
			
		||||
    content.widget.attrs.update({"class": "textarea"})
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Comment
 | 
			
		||||
        model = models.Comment
 | 
			
		||||
        fields = ["nickname", "email", "content"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgramForm(ModelForm):
 | 
			
		||||
    content = RichTextField()
 | 
			
		||||
    new_cover = ImageField(required=False)
 | 
			
		||||
class ImageForm(forms.Form):
 | 
			
		||||
    file = forms.ImageField()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PageForm(forms.ModelForm):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        fields = ("title", "status", "cover", "content")
 | 
			
		||||
        model = models.Page
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgramForm(PageForm):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        fields = PageForm.Meta.fields
 | 
			
		||||
        model = models.Program
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EpisodeForm(PageForm):
 | 
			
		||||
    new_podcast = forms.FileField(required=False)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Program
 | 
			
		||||
        fields = ["content"]
 | 
			
		||||
 | 
			
		||||
    def save(self, commit=True):
 | 
			
		||||
        file_obj = self.cleaned_data["new_cover"]
 | 
			
		||||
        if file_obj:
 | 
			
		||||
            obj, _ = Image.objects.get_or_create(original_filename=file_obj.name, file=file_obj)
 | 
			
		||||
            self.instance.cover = obj
 | 
			
		||||
        super().save(commit=commit)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EpisodeForm(ModelForm):
 | 
			
		||||
    content = RichTextField()
 | 
			
		||||
    new_podcast = FileField(required=False)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Episode
 | 
			
		||||
        model = models.Episode
 | 
			
		||||
        fields = ["content"]
 | 
			
		||||
 | 
			
		||||
    def save(self, commit=True):
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ class AircoxMiddleware(object):
 | 
			
		||||
    """Middleware used to get default info for the given website.
 | 
			
		||||
 | 
			
		||||
    It provide following request attributes:
 | 
			
		||||
    - ``mobile``: set to True if mobile device is detected
 | 
			
		||||
    - ``station``: current Station
 | 
			
		||||
 | 
			
		||||
    This middleware must be set after the middleware
 | 
			
		||||
@ -24,6 +25,11 @@ class AircoxMiddleware(object):
 | 
			
		||||
    def __init__(self, get_response):
 | 
			
		||||
        self.get_response = get_response
 | 
			
		||||
 | 
			
		||||
    def is_mobile(self, request):
 | 
			
		||||
        if agent := request.META.get("HTTP_USER_AGENT"):
 | 
			
		||||
            return " Mobi" in agent
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def get_station(self, request):
 | 
			
		||||
        """Return station for the provided request."""
 | 
			
		||||
        host = request.get_host()
 | 
			
		||||
@ -45,6 +51,7 @@ class AircoxMiddleware(object):
 | 
			
		||||
    def __call__(self, request):
 | 
			
		||||
        self.init_timezone(request)
 | 
			
		||||
        request.station = self.get_station(request)
 | 
			
		||||
        request.is_mobile = self.is_mobile(request)
 | 
			
		||||
        try:
 | 
			
		||||
            return self.get_response(request)
 | 
			
		||||
        except Redirect:
 | 
			
		||||
 | 
			
		||||
@ -127,9 +127,9 @@ class Program(Page):
 | 
			
		||||
            self.editors = editors
 | 
			
		||||
            super().save()
 | 
			
		||||
        permission, _ = Permission.objects.get_or_create(
 | 
			
		||||
            name=f"change program {self.title}",
 | 
			
		||||
            codename=self.change_permission_codename,
 | 
			
		||||
            content_type=ContentType.objects.get_for_model(self),
 | 
			
		||||
            defaults={"name": f"change program {self.title}"},
 | 
			
		||||
        )
 | 
			
		||||
        if permission not in editors.permissions.all():
 | 
			
		||||
            editors.permissions.add(permission)
 | 
			
		||||
 | 
			
		||||
@ -81,6 +81,9 @@ class Station(models.Model):
 | 
			
		||||
        max_length=64,
 | 
			
		||||
        default=_("Music stream"),
 | 
			
		||||
    )
 | 
			
		||||
    legal_label = models.CharField(
 | 
			
		||||
        _("Legal label"), max_length=64, blank=True, default="", help_text=_("Displayed at the bottom of pages.")
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    objects = StationQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,17 @@
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from filer.models.imagemodels import Image
 | 
			
		||||
from taggit.serializers import TaggitSerializer, TagListSerializerField
 | 
			
		||||
 | 
			
		||||
from ..models import Track, UserSettings
 | 
			
		||||
 | 
			
		||||
__all__ = ("TrackSerializer", "UserSettingsSerializer")
 | 
			
		||||
__all__ = ("ImageSerializer", "TrackSerializer", "UserSettingsSerializer")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ImageSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Image
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TrackSerializer(TaggitSerializer, serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
@ -27,8 +27,8 @@
 | 
			
		||||
  --preview-cover-size: 14rem;
 | 
			
		||||
  --preview-cover-small-size: 10rem;
 | 
			
		||||
  --preview-cover-tiny-size: 4rem;
 | 
			
		||||
  --preview-wide-content-sz: 1.6rem;
 | 
			
		||||
  --preview-heading-bg-color: var(--hg-color);
 | 
			
		||||
  --preview-wide-content-sz: 1.2rem;
 | 
			
		||||
  --preview-heading-bg-color: var(--main-color);
 | 
			
		||||
  --header-height: var(--cover-h);
 | 
			
		||||
  --a-carousel-p: 1.4rem;
 | 
			
		||||
  --a-carousel-ml: calc(1.2rem - 0.5rem);
 | 
			
		||||
@ -76,9 +76,6 @@
 | 
			
		||||
    --cover-tiny-w: 4rem;
 | 
			
		||||
    --cover-tiny-h: 4rem;
 | 
			
		||||
    --section-content-sz: 1rem;
 | 
			
		||||
    --preview-title-sz: 1rem;
 | 
			
		||||
    --preview-subtitle-sz: 0.8rem;
 | 
			
		||||
    --preview-wide-content-sz: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.title.is-1, .header.preview .title.is-1 {
 | 
			
		||||
@ -124,6 +121,10 @@
 | 
			
		||||
  color: var(--heading-hg-fg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.panels .panel:not(.active) {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.preview {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  background-size: cover;
 | 
			
		||||
@ -288,7 +289,7 @@
 | 
			
		||||
  transition: box-shadow 0.2s;
 | 
			
		||||
}
 | 
			
		||||
.preview-card:hover figure {
 | 
			
		||||
  box-shadow: 0em 0em 1.2em rgba(0, 0, 0, 0.4) !important;
 | 
			
		||||
  box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2);
 | 
			
		||||
}
 | 
			
		||||
.preview-card:hover a {
 | 
			
		||||
  color: var(--heading-link-hv-fg);
 | 
			
		||||
@ -299,9 +300,6 @@
 | 
			
		||||
.preview-card .headings .heading {
 | 
			
		||||
  display: block !important;
 | 
			
		||||
}
 | 
			
		||||
.preview-card .headings .title {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
.preview-card .headings .subtitle {
 | 
			
		||||
  font-size: 1.2rem;
 | 
			
		||||
}
 | 
			
		||||
@ -310,7 +308,6 @@
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
.preview-card .card-content figure {
 | 
			
		||||
  box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2);
 | 
			
		||||
  height: var(--cover-h);
 | 
			
		||||
  width: var(--cover-w);
 | 
			
		||||
}
 | 
			
		||||
@ -321,18 +318,6 @@
 | 
			
		||||
  right: 0rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-grid {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr 1fr;
 | 
			
		||||
  grid-auto-flow: dense;
 | 
			
		||||
  gap: 1.2rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 900px) {
 | 
			
		||||
  .list-grid {
 | 
			
		||||
    grid-template-columns: 1fr;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.a-carousel .a-carousel-viewport {
 | 
			
		||||
  box-shadow: inset 0em 0em 20rem var(--a-carousel-bg);
 | 
			
		||||
  padding: 0rem;
 | 
			
		||||
@ -544,6 +529,34 @@
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.a-select-file > *:not(:last-child) {
 | 
			
		||||
  margin-bottom: 0.6rem;
 | 
			
		||||
}
 | 
			
		||||
.a-select-file .upload-preview {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
}
 | 
			
		||||
.a-select-file .a-select-file-list {
 | 
			
		||||
  max-height: 30rem;
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr 1fr 1fr 1fr;
 | 
			
		||||
  gap: 0.6rem;
 | 
			
		||||
}
 | 
			
		||||
.a-select-file .file-preview {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
.a-select-file .file-preview:hover {
 | 
			
		||||
  box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2);
 | 
			
		||||
}
 | 
			
		||||
.a-select-file .file-preview.active {
 | 
			
		||||
  box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.4);
 | 
			
		||||
}
 | 
			
		||||
.a-select-file .file-preview img {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  max-height: 10rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Bulma Utilities */
 | 
			
		||||
.button {
 | 
			
		||||
  -moz-appearance: none;
 | 
			
		||||
@ -2795,9 +2808,9 @@ a.navbar-item:focus, a.navbar-item:focus-within, a.navbar-item:hover, a.navbar-i
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
#player .button[disabled], #player .button.disabled, #player a.button[disabled], #player a.button.disabled, #player button.button[disabled], #player button.button.disabled, .ax .button[disabled], .ax .button.disabled, .ax a.button[disabled], .ax a.button.disabled, .ax button.button[disabled], .ax button.button.disabled {
 | 
			
		||||
  background-color: var(--hg-color-grey);
 | 
			
		||||
  color: var(--hg-color-2);
 | 
			
		||||
  border-color: var(--hg-color-2-alpha);
 | 
			
		||||
  background-color: var(--text-color-light);
 | 
			
		||||
  color: var(--secondary-color);
 | 
			
		||||
  border-color: var(--secondary-color-light);
 | 
			
		||||
}
 | 
			
		||||
#player .button .dropdown-trigger, #player a.button .dropdown-trigger, #player button.button .dropdown-trigger, .ax .button .dropdown-trigger, .ax a.button .dropdown-trigger, .ax button.button .dropdown-trigger {
 | 
			
		||||
  border-radius: 1.5em;
 | 
			
		||||
 | 
			
		||||
@ -98,7 +98,7 @@ fieldset[disabled] .file-name, fieldset[disabled] .select select, .select fields
 | 
			
		||||
  width: 0.625em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-container:not(:last-child), .table:not(:last-child), .content:not(:last-child), .box:not(:last-child), .message:not(:last-child) {
 | 
			
		||||
.table-container:not(:last-child), .table:not(:last-child), .box:not(:last-child), .message:not(:last-child) {
 | 
			
		||||
  margin-bottom: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6238,175 +6238,6 @@ a.box:active {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.content li + li {
 | 
			
		||||
  margin-top: 0.25em;
 | 
			
		||||
}
 | 
			
		||||
.content p:not(:last-child),
 | 
			
		||||
.content dl:not(:last-child),
 | 
			
		||||
.content ol:not(:last-child),
 | 
			
		||||
.content ul:not(:last-child),
 | 
			
		||||
.content blockquote:not(:last-child),
 | 
			
		||||
.content pre:not(:last-child),
 | 
			
		||||
.content table:not(:last-child) {
 | 
			
		||||
  margin-bottom: 1em;
 | 
			
		||||
}
 | 
			
		||||
.content h1,
 | 
			
		||||
.content h2,
 | 
			
		||||
.content h3,
 | 
			
		||||
.content h4,
 | 
			
		||||
.content h5,
 | 
			
		||||
.content h6 {
 | 
			
		||||
  color: hsl(0deg, 0%, 21%);
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  line-height: 1.125;
 | 
			
		||||
}
 | 
			
		||||
.content h1 {
 | 
			
		||||
  font-size: 2em;
 | 
			
		||||
  margin-bottom: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
.content h1:not(:first-child) {
 | 
			
		||||
  margin-top: 1em;
 | 
			
		||||
}
 | 
			
		||||
.content h2 {
 | 
			
		||||
  font-size: 1.75em;
 | 
			
		||||
  margin-bottom: 0.5714em;
 | 
			
		||||
}
 | 
			
		||||
.content h2:not(:first-child) {
 | 
			
		||||
  margin-top: 1.1428em;
 | 
			
		||||
}
 | 
			
		||||
.content h3 {
 | 
			
		||||
  font-size: 1.5em;
 | 
			
		||||
  margin-bottom: 0.6666em;
 | 
			
		||||
}
 | 
			
		||||
.content h3:not(:first-child) {
 | 
			
		||||
  margin-top: 1.3333em;
 | 
			
		||||
}
 | 
			
		||||
.content h4 {
 | 
			
		||||
  font-size: 1.25em;
 | 
			
		||||
  margin-bottom: 0.8em;
 | 
			
		||||
}
 | 
			
		||||
.content h5 {
 | 
			
		||||
  font-size: 1.125em;
 | 
			
		||||
  margin-bottom: 0.8888em;
 | 
			
		||||
}
 | 
			
		||||
.content h6 {
 | 
			
		||||
  font-size: 1em;
 | 
			
		||||
  margin-bottom: 1em;
 | 
			
		||||
}
 | 
			
		||||
.content blockquote {
 | 
			
		||||
  background-color: hsl(0deg, 0%, 96%);
 | 
			
		||||
  border-left: 5px solid hsl(0deg, 0%, 86%);
 | 
			
		||||
  padding: 1.25em 1.5em;
 | 
			
		||||
}
 | 
			
		||||
.content ol {
 | 
			
		||||
  list-style-position: outside;
 | 
			
		||||
  margin-left: 2em;
 | 
			
		||||
  margin-top: 1em;
 | 
			
		||||
}
 | 
			
		||||
.content ol:not([type]) {
 | 
			
		||||
  list-style-type: decimal;
 | 
			
		||||
}
 | 
			
		||||
.content ol:not([type]).is-lower-alpha {
 | 
			
		||||
  list-style-type: lower-alpha;
 | 
			
		||||
}
 | 
			
		||||
.content ol:not([type]).is-lower-roman {
 | 
			
		||||
  list-style-type: lower-roman;
 | 
			
		||||
}
 | 
			
		||||
.content ol:not([type]).is-upper-alpha {
 | 
			
		||||
  list-style-type: upper-alpha;
 | 
			
		||||
}
 | 
			
		||||
.content ol:not([type]).is-upper-roman {
 | 
			
		||||
  list-style-type: upper-roman;
 | 
			
		||||
}
 | 
			
		||||
.content ul {
 | 
			
		||||
  list-style: disc outside;
 | 
			
		||||
  margin-left: 2em;
 | 
			
		||||
  margin-top: 1em;
 | 
			
		||||
}
 | 
			
		||||
.content ul ul {
 | 
			
		||||
  list-style-type: circle;
 | 
			
		||||
  margin-top: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
.content ul ul ul {
 | 
			
		||||
  list-style-type: square;
 | 
			
		||||
}
 | 
			
		||||
.content dd {
 | 
			
		||||
  margin-left: 2em;
 | 
			
		||||
}
 | 
			
		||||
.content figure {
 | 
			
		||||
  margin-left: 2em;
 | 
			
		||||
  margin-right: 2em;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
.content figure:not(:first-child) {
 | 
			
		||||
  margin-top: 2em;
 | 
			
		||||
}
 | 
			
		||||
.content figure:not(:last-child) {
 | 
			
		||||
  margin-bottom: 2em;
 | 
			
		||||
}
 | 
			
		||||
.content figure img {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
.content figure figcaption {
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
}
 | 
			
		||||
.content pre {
 | 
			
		||||
  -webkit-overflow-scrolling: touch;
 | 
			
		||||
  overflow-x: auto;
 | 
			
		||||
  padding: 1.25em 1.5em;
 | 
			
		||||
  white-space: pre;
 | 
			
		||||
  word-wrap: normal;
 | 
			
		||||
}
 | 
			
		||||
.content sup,
 | 
			
		||||
.content sub {
 | 
			
		||||
  font-size: 75%;
 | 
			
		||||
}
 | 
			
		||||
.content table {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
.content table td,
 | 
			
		||||
.content table th {
 | 
			
		||||
  border: 1px solid hsl(0deg, 0%, 86%);
 | 
			
		||||
  border-width: 0 0 1px;
 | 
			
		||||
  padding: 0.5em 0.75em;
 | 
			
		||||
  vertical-align: top;
 | 
			
		||||
}
 | 
			
		||||
.content table th {
 | 
			
		||||
  color: hsl(0deg, 0%, 21%);
 | 
			
		||||
}
 | 
			
		||||
.content table th:not([align]) {
 | 
			
		||||
  text-align: inherit;
 | 
			
		||||
}
 | 
			
		||||
.content table thead td,
 | 
			
		||||
.content table thead th {
 | 
			
		||||
  border-width: 0 0 2px;
 | 
			
		||||
  color: hsl(0deg, 0%, 21%);
 | 
			
		||||
}
 | 
			
		||||
.content table tfoot td,
 | 
			
		||||
.content table tfoot th {
 | 
			
		||||
  border-width: 2px 0 0;
 | 
			
		||||
  color: hsl(0deg, 0%, 21%);
 | 
			
		||||
}
 | 
			
		||||
.content table tbody tr:last-child td,
 | 
			
		||||
.content table tbody tr:last-child th {
 | 
			
		||||
  border-bottom-width: 0;
 | 
			
		||||
}
 | 
			
		||||
.content .tabs li + li {
 | 
			
		||||
  margin-top: 0;
 | 
			
		||||
}
 | 
			
		||||
.content.is-small {
 | 
			
		||||
  font-size: 0.75rem;
 | 
			
		||||
}
 | 
			
		||||
.content.is-normal {
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
}
 | 
			
		||||
.content.is-medium {
 | 
			
		||||
  font-size: 1.25rem;
 | 
			
		||||
}
 | 
			
		||||
.content.is-large {
 | 
			
		||||
  font-size: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon {
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
@ -6812,6 +6643,11 @@ a.tag:hover {
 | 
			
		||||
  text-decoration: underline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-light {
 | 
			
		||||
  weight: 400;
 | 
			
		||||
  color: var(--text-color-light);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.align-left {
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  justify-content: left;
 | 
			
		||||
@ -6834,6 +6670,10 @@ a.tag:hover {
 | 
			
		||||
  clear: both !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clear-unset {
 | 
			
		||||
  clear: unset !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.d-inline {
 | 
			
		||||
  display: inline !important;
 | 
			
		||||
}
 | 
			
		||||
@ -6846,21 +6686,58 @@ a.tag:hover {
 | 
			
		||||
  display: inline-block !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.push-right, .flex-push-right {
 | 
			
		||||
  margin-left: auto !important;
 | 
			
		||||
.p-relative {
 | 
			
		||||
  position: relative !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.push-bottom {
 | 
			
		||||
  margin-top: auto !important;
 | 
			
		||||
.p-absolute {
 | 
			
		||||
  position: absolute !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.p-fixed {
 | 
			
		||||
  position: fixed !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.p-sticky {
 | 
			
		||||
  position: sticky !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.p-static {
 | 
			
		||||
  position: static !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ws-nowrap {
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.grid {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr 1fr;
 | 
			
		||||
  grid-auto-flow: dense;
 | 
			
		||||
  gap: 1.2rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.grid-1 {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr 1fr;
 | 
			
		||||
  grid-auto-flow: dense;
 | 
			
		||||
  gap: 1.2rem;
 | 
			
		||||
  grid-template-columns: 1fr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.grid-2 {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr 1fr;
 | 
			
		||||
  grid-auto-flow: dense;
 | 
			
		||||
  gap: 1.2rem;
 | 
			
		||||
  grid-template-columns: 1fr 1fr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.grid-3 {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr 1fr;
 | 
			
		||||
  grid-auto-flow: dense;
 | 
			
		||||
  gap: 1.2rem;
 | 
			
		||||
  grid-template-columns: 1fr 1fr 1fr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6905,8 +6782,8 @@ a.tag:hover {
 | 
			
		||||
  border-radius: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.is-borderless {
 | 
			
		||||
  border: none;
 | 
			
		||||
.no-border {
 | 
			
		||||
  border: 0px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.overflow-hidden {
 | 
			
		||||
@ -6917,38 +6794,10 @@ a.tag:hover {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.p-relative {
 | 
			
		||||
  position: relative !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.p-absolute {
 | 
			
		||||
  position: absolute !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.p-fixed {
 | 
			
		||||
  position: fixed !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.p-sticky {
 | 
			
		||||
  position: sticky !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.p-static {
 | 
			
		||||
  position: static !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.height-full {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ws-nowrap {
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.no-border {
 | 
			
		||||
  border: 0px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
*[draggable=true] {
 | 
			
		||||
  cursor: move;
 | 
			
		||||
}
 | 
			
		||||
@ -6969,12 +6818,12 @@ a.tag:hover {
 | 
			
		||||
  animation: 1s ease-in-out 1s infinite alternate blink;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.hg-color {
 | 
			
		||||
  color: var(--highlight-color);
 | 
			
		||||
.main-color {
 | 
			
		||||
  color: var(--main-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.hg-color-2 {
 | 
			
		||||
  color: var(--highlight-color-2);
 | 
			
		||||
.secondary-color {
 | 
			
		||||
  color: var(--secondary-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bg-transparent {
 | 
			
		||||
@ -7012,12 +6861,6 @@ input.half-field:not(:active):not(:hover) {
 | 
			
		||||
  --disabled-bg: #eee;
 | 
			
		||||
  --link-fg: #00A6A6;
 | 
			
		||||
  --link-hv-fg: var(--text-color);
 | 
			
		||||
  --hg-color: #EFCA08;
 | 
			
		||||
  --hg-color-alpha: #EFCA08B3;
 | 
			
		||||
  --hg-color-grey: rgba(230, 230, 60, 1);
 | 
			
		||||
  --hg-color-2: #F49F0A;
 | 
			
		||||
  --hg-color-2-alpha: #F49F0AB3;
 | 
			
		||||
  --hg-color-2-grey: rgba(50, 200, 200, 1);
 | 
			
		||||
  --nav-primary-height: 3rem;
 | 
			
		||||
  --nav-secondary-height: 2.5rem;
 | 
			
		||||
  --nav-fg: var(--text-color);
 | 
			
		||||
@ -7031,22 +6874,36 @@ input.half-field:not(:active):not(:hover) {
 | 
			
		||||
  --nav-2-fs: 0.9rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
  font-size: 1.4em;
 | 
			
		||||
  background-color: var(--body-bg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 1280px) {
 | 
			
		||||
  body {
 | 
			
		||||
    font-size: 1.2em;
 | 
			
		||||
body.mobile .grid {
 | 
			
		||||
  grid-template-columns: 1fr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 900px) {
 | 
			
		||||
  .grid {
 | 
			
		||||
    grid-template-columns: 1fr;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@media screen and (max-width: 1024px) {
 | 
			
		||||
  body {
 | 
			
		||||
    font-size: 1em;
 | 
			
		||||
  html {
 | 
			
		||||
    font-size: 18px !important;
 | 
			
		||||
  }
 | 
			
		||||
  :root {
 | 
			
		||||
    --header-height: 20rem;
 | 
			
		||||
}
 | 
			
		||||
@media screen and (max-width: 1280px) {
 | 
			
		||||
  html {
 | 
			
		||||
    font-size: 20px !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@media screen and (min-width: 1280px) {
 | 
			
		||||
  html {
 | 
			
		||||
    font-size: 24px !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
h1, h2, h3, h4, h5, h6, .heading, .title, .subtitle {
 | 
			
		||||
@ -7056,3 +6913,8 @@ h1, h2, h3, h4, h5, h6, .heading, .title, .subtitle {
 | 
			
		||||
.container:empty {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.header-cover {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1211,7 +1211,7 @@
 | 
			
		||||
  background-color: var(--vc-bg);
 | 
			
		||||
  -webkit-font-smoothing: antialiased;
 | 
			
		||||
  -moz-osx-font-smoothing: grayscale;
 | 
			
		||||
  -webkit-tap-hg-color: transparent;
 | 
			
		||||
  -webkit-tap-highlight-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vc-container,
 | 
			
		||||
 | 
			
		||||
@ -27,8 +27,8 @@
 | 
			
		||||
  --preview-cover-size: 14rem;
 | 
			
		||||
  --preview-cover-small-size: 10rem;
 | 
			
		||||
  --preview-cover-tiny-size: 4rem;
 | 
			
		||||
  --preview-wide-content-sz: 1.6rem;
 | 
			
		||||
  --preview-heading-bg-color: var(--hg-color);
 | 
			
		||||
  --preview-wide-content-sz: 1.2rem;
 | 
			
		||||
  --preview-heading-bg-color: var(--main-color);
 | 
			
		||||
  --header-height: var(--cover-h);
 | 
			
		||||
  --a-carousel-p: 1.4rem;
 | 
			
		||||
  --a-carousel-ml: calc(1.2rem - 0.5rem);
 | 
			
		||||
@ -76,9 +76,6 @@
 | 
			
		||||
    --cover-tiny-w: 4rem;
 | 
			
		||||
    --cover-tiny-h: 4rem;
 | 
			
		||||
    --section-content-sz: 1rem;
 | 
			
		||||
    --preview-title-sz: 1rem;
 | 
			
		||||
    --preview-subtitle-sz: 0.8rem;
 | 
			
		||||
    --preview-wide-content-sz: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.title.is-1, .header.preview .title.is-1 {
 | 
			
		||||
@ -124,6 +121,10 @@
 | 
			
		||||
  color: var(--heading-hg-fg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.panels .panel:not(.active) {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.preview {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  background-size: cover;
 | 
			
		||||
@ -288,7 +289,7 @@
 | 
			
		||||
  transition: box-shadow 0.2s;
 | 
			
		||||
}
 | 
			
		||||
.preview-card:hover figure {
 | 
			
		||||
  box-shadow: 0em 0em 1.2em rgba(0, 0, 0, 0.4) !important;
 | 
			
		||||
  box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2);
 | 
			
		||||
}
 | 
			
		||||
.preview-card:hover a {
 | 
			
		||||
  color: var(--heading-link-hv-fg);
 | 
			
		||||
@ -299,9 +300,6 @@
 | 
			
		||||
.preview-card .headings .heading {
 | 
			
		||||
  display: block !important;
 | 
			
		||||
}
 | 
			
		||||
.preview-card .headings .title {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
.preview-card .headings .subtitle {
 | 
			
		||||
  font-size: 1.2rem;
 | 
			
		||||
}
 | 
			
		||||
@ -310,7 +308,6 @@
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
.preview-card .card-content figure {
 | 
			
		||||
  box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2);
 | 
			
		||||
  height: var(--cover-h);
 | 
			
		||||
  width: var(--cover-w);
 | 
			
		||||
}
 | 
			
		||||
@ -321,18 +318,6 @@
 | 
			
		||||
  right: 0rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-grid {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr 1fr;
 | 
			
		||||
  grid-auto-flow: dense;
 | 
			
		||||
  gap: 1.2rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 900px) {
 | 
			
		||||
  .list-grid {
 | 
			
		||||
    grid-template-columns: 1fr;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.a-carousel .a-carousel-viewport {
 | 
			
		||||
  box-shadow: inset 0em 0em 20rem var(--a-carousel-bg);
 | 
			
		||||
  padding: 0rem;
 | 
			
		||||
@ -544,6 +529,34 @@
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.a-select-file > *:not(:last-child) {
 | 
			
		||||
  margin-bottom: 0.6rem;
 | 
			
		||||
}
 | 
			
		||||
.a-select-file .upload-preview {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
}
 | 
			
		||||
.a-select-file .a-select-file-list {
 | 
			
		||||
  max-height: 30rem;
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr 1fr 1fr 1fr;
 | 
			
		||||
  gap: 0.6rem;
 | 
			
		||||
}
 | 
			
		||||
.a-select-file .file-preview {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
.a-select-file .file-preview:hover {
 | 
			
		||||
  box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2);
 | 
			
		||||
}
 | 
			
		||||
.a-select-file .file-preview.active {
 | 
			
		||||
  box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.4);
 | 
			
		||||
}
 | 
			
		||||
.a-select-file .file-preview img {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  max-height: 10rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Bulma Utilities */
 | 
			
		||||
.file-cta,
 | 
			
		||||
.file-name, .select select, .textarea, .input {
 | 
			
		||||
@ -602,7 +615,7 @@ fieldset[disabled] .file-name, fieldset[disabled] .select select, .select fields
 | 
			
		||||
  width: 0.625em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-container:not(:last-child), .table:not(:last-child), .content:not(:last-child), .box:not(:last-child), .message:not(:last-child) {
 | 
			
		||||
.table-container:not(:last-child), .table:not(:last-child), .box:not(:last-child), .message:not(:last-child) {
 | 
			
		||||
  margin-bottom: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6742,175 +6755,6 @@ a.box:active {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.content li + li {
 | 
			
		||||
  margin-top: 0.25em;
 | 
			
		||||
}
 | 
			
		||||
.content p:not(:last-child),
 | 
			
		||||
.content dl:not(:last-child),
 | 
			
		||||
.content ol:not(:last-child),
 | 
			
		||||
.content ul:not(:last-child),
 | 
			
		||||
.content blockquote:not(:last-child),
 | 
			
		||||
.content pre:not(:last-child),
 | 
			
		||||
.content table:not(:last-child) {
 | 
			
		||||
  margin-bottom: 1em;
 | 
			
		||||
}
 | 
			
		||||
.content h1,
 | 
			
		||||
.content h2,
 | 
			
		||||
.content h3,
 | 
			
		||||
.content h4,
 | 
			
		||||
.content h5,
 | 
			
		||||
.content h6 {
 | 
			
		||||
  color: hsl(0deg, 0%, 21%);
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  line-height: 1.125;
 | 
			
		||||
}
 | 
			
		||||
.content h1 {
 | 
			
		||||
  font-size: 2em;
 | 
			
		||||
  margin-bottom: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
.content h1:not(:first-child) {
 | 
			
		||||
  margin-top: 1em;
 | 
			
		||||
}
 | 
			
		||||
.content h2 {
 | 
			
		||||
  font-size: 1.75em;
 | 
			
		||||
  margin-bottom: 0.5714em;
 | 
			
		||||
}
 | 
			
		||||
.content h2:not(:first-child) {
 | 
			
		||||
  margin-top: 1.1428em;
 | 
			
		||||
}
 | 
			
		||||
.content h3 {
 | 
			
		||||
  font-size: 1.5em;
 | 
			
		||||
  margin-bottom: 0.6666em;
 | 
			
		||||
}
 | 
			
		||||
.content h3:not(:first-child) {
 | 
			
		||||
  margin-top: 1.3333em;
 | 
			
		||||
}
 | 
			
		||||
.content h4 {
 | 
			
		||||
  font-size: 1.25em;
 | 
			
		||||
  margin-bottom: 0.8em;
 | 
			
		||||
}
 | 
			
		||||
.content h5 {
 | 
			
		||||
  font-size: 1.125em;
 | 
			
		||||
  margin-bottom: 0.8888em;
 | 
			
		||||
}
 | 
			
		||||
.content h6 {
 | 
			
		||||
  font-size: 1em;
 | 
			
		||||
  margin-bottom: 1em;
 | 
			
		||||
}
 | 
			
		||||
.content blockquote {
 | 
			
		||||
  background-color: hsl(0deg, 0%, 96%);
 | 
			
		||||
  border-left: 5px solid hsl(0deg, 0%, 86%);
 | 
			
		||||
  padding: 1.25em 1.5em;
 | 
			
		||||
}
 | 
			
		||||
.content ol {
 | 
			
		||||
  list-style-position: outside;
 | 
			
		||||
  margin-left: 2em;
 | 
			
		||||
  margin-top: 1em;
 | 
			
		||||
}
 | 
			
		||||
.content ol:not([type]) {
 | 
			
		||||
  list-style-type: decimal;
 | 
			
		||||
}
 | 
			
		||||
.content ol:not([type]).is-lower-alpha {
 | 
			
		||||
  list-style-type: lower-alpha;
 | 
			
		||||
}
 | 
			
		||||
.content ol:not([type]).is-lower-roman {
 | 
			
		||||
  list-style-type: lower-roman;
 | 
			
		||||
}
 | 
			
		||||
.content ol:not([type]).is-upper-alpha {
 | 
			
		||||
  list-style-type: upper-alpha;
 | 
			
		||||
}
 | 
			
		||||
.content ol:not([type]).is-upper-roman {
 | 
			
		||||
  list-style-type: upper-roman;
 | 
			
		||||
}
 | 
			
		||||
.content ul {
 | 
			
		||||
  list-style: disc outside;
 | 
			
		||||
  margin-left: 2em;
 | 
			
		||||
  margin-top: 1em;
 | 
			
		||||
}
 | 
			
		||||
.content ul ul {
 | 
			
		||||
  list-style-type: circle;
 | 
			
		||||
  margin-top: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
.content ul ul ul {
 | 
			
		||||
  list-style-type: square;
 | 
			
		||||
}
 | 
			
		||||
.content dd {
 | 
			
		||||
  margin-left: 2em;
 | 
			
		||||
}
 | 
			
		||||
.content figure {
 | 
			
		||||
  margin-left: 2em;
 | 
			
		||||
  margin-right: 2em;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
.content figure:not(:first-child) {
 | 
			
		||||
  margin-top: 2em;
 | 
			
		||||
}
 | 
			
		||||
.content figure:not(:last-child) {
 | 
			
		||||
  margin-bottom: 2em;
 | 
			
		||||
}
 | 
			
		||||
.content figure img {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
.content figure figcaption {
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
}
 | 
			
		||||
.content pre {
 | 
			
		||||
  -webkit-overflow-scrolling: touch;
 | 
			
		||||
  overflow-x: auto;
 | 
			
		||||
  padding: 1.25em 1.5em;
 | 
			
		||||
  white-space: pre;
 | 
			
		||||
  word-wrap: normal;
 | 
			
		||||
}
 | 
			
		||||
.content sup,
 | 
			
		||||
.content sub {
 | 
			
		||||
  font-size: 75%;
 | 
			
		||||
}
 | 
			
		||||
.content table {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
.content table td,
 | 
			
		||||
.content table th {
 | 
			
		||||
  border: 1px solid hsl(0deg, 0%, 86%);
 | 
			
		||||
  border-width: 0 0 1px;
 | 
			
		||||
  padding: 0.5em 0.75em;
 | 
			
		||||
  vertical-align: top;
 | 
			
		||||
}
 | 
			
		||||
.content table th {
 | 
			
		||||
  color: hsl(0deg, 0%, 21%);
 | 
			
		||||
}
 | 
			
		||||
.content table th:not([align]) {
 | 
			
		||||
  text-align: inherit;
 | 
			
		||||
}
 | 
			
		||||
.content table thead td,
 | 
			
		||||
.content table thead th {
 | 
			
		||||
  border-width: 0 0 2px;
 | 
			
		||||
  color: hsl(0deg, 0%, 21%);
 | 
			
		||||
}
 | 
			
		||||
.content table tfoot td,
 | 
			
		||||
.content table tfoot th {
 | 
			
		||||
  border-width: 2px 0 0;
 | 
			
		||||
  color: hsl(0deg, 0%, 21%);
 | 
			
		||||
}
 | 
			
		||||
.content table tbody tr:last-child td,
 | 
			
		||||
.content table tbody tr:last-child th {
 | 
			
		||||
  border-bottom-width: 0;
 | 
			
		||||
}
 | 
			
		||||
.content .tabs li + li {
 | 
			
		||||
  margin-top: 0;
 | 
			
		||||
}
 | 
			
		||||
.content.is-small {
 | 
			
		||||
  font-size: 0.75rem;
 | 
			
		||||
}
 | 
			
		||||
.content.is-normal {
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
}
 | 
			
		||||
.content.is-medium {
 | 
			
		||||
  font-size: 1.25rem;
 | 
			
		||||
}
 | 
			
		||||
.content.is-large {
 | 
			
		||||
  font-size: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon {
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
@ -7348,7 +7192,7 @@ a.tag:hover {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vc-weekday-1, .vc-weekday-7 {
 | 
			
		||||
  color: var(--hg-color-2) !important;
 | 
			
		||||
  color: var(--secondary-color) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.schedules {
 | 
			
		||||
@ -7411,9 +7255,9 @@ a.tag:hover {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
.button[disabled], .button.disabled, a.button[disabled], a.button.disabled, button.button[disabled], button.button.disabled {
 | 
			
		||||
  background-color: var(--hg-color-grey);
 | 
			
		||||
  color: var(--hg-color-2);
 | 
			
		||||
  border-color: var(--hg-color-2-alpha);
 | 
			
		||||
  background-color: var(--text-color-light);
 | 
			
		||||
  color: var(--secondary-color);
 | 
			
		||||
  border-color: var(--secondary-color-light);
 | 
			
		||||
}
 | 
			
		||||
.button .dropdown-trigger, a.button .dropdown-trigger, button.button .dropdown-trigger {
 | 
			
		||||
  border-radius: 1.5em;
 | 
			
		||||
@ -7481,8 +7325,8 @@ a.tag:hover {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.navbar-item.active, .table tr.is-selected {
 | 
			
		||||
  color: var(--hg-color-2);
 | 
			
		||||
  background-color: var(--hg-color);
 | 
			
		||||
  color: var(--secondary-color);
 | 
			
		||||
  background-color: var(--main-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.title {
 | 
			
		||||
@ -7743,15 +7587,6 @@ nav li a, nav li .button {
 | 
			
		||||
  border-color: var(--secondary-color-dark) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
  font-size: 1.4em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 1380px) {
 | 
			
		||||
  body {
 | 
			
		||||
    font-size: 1em;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@media screen and (max-width: 1024px) {
 | 
			
		||||
  .page .container {
 | 
			
		||||
    margin-left: 1.2rem;
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -1,29 +0,0 @@
 | 
			
		||||
/* global CKEDITOR, django */
 | 
			
		||||
/* Modified in order to be manually loaded after vue.js */
 | 
			
		||||
 | 
			
		||||
  function initialiseCKEditor() {
 | 
			
		||||
    var textareas = Array.prototype.slice.call(
 | 
			
		||||
      document.querySelectorAll("textarea[data-type=ckeditortype]"),
 | 
			
		||||
    )
 | 
			
		||||
    for (var i = 0; i < textareas.length; ++i) {
 | 
			
		||||
      var t = textareas[i]
 | 
			
		||||
      if (
 | 
			
		||||
        t.getAttribute("data-processed") == "0" &&
 | 
			
		||||
        t.id.indexOf("__prefix__") == -1
 | 
			
		||||
      ) {
 | 
			
		||||
        t.setAttribute("data-processed", "1")
 | 
			
		||||
        var ext = JSON.parse(t.getAttribute("data-external-plugin-resources"))
 | 
			
		||||
        for (var j = 0; j < ext.length; ++j) {
 | 
			
		||||
          CKEDITOR.plugins.addExternal(ext[j][0], ext[j][1], ext[j][2])
 | 
			
		||||
        }
 | 
			
		||||
        CKEDITOR.replace(t.id, JSON.parse(t.getAttribute("data-config")))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function initialiseCKEditorInInlinedForms() {
 | 
			
		||||
    if (typeof django === "object" && django.jQuery) {
 | 
			
		||||
      django.jQuery(document).on("formset:added", initialiseCKEditor)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
//})()
 | 
			
		||||
@ -7,9 +7,6 @@ Usefull context:
 | 
			
		||||
- cover: image cover
 | 
			
		||||
- site: current website
 | 
			
		||||
- model: view model or displayed `object`'s
 | 
			
		||||
- sidebar_object_list: item to display in sidebar
 | 
			
		||||
- sidebar_url_name: url name sidebar item complete list
 | 
			
		||||
- sidebar_url_parent: parent page for sidebar items complete list
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
<html>
 | 
			
		||||
    <head>
 | 
			
		||||
@ -39,7 +36,7 @@ Usefull context:
 | 
			
		||||
 | 
			
		||||
        {% block head_extra %}{% endblock %}
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>
 | 
			
		||||
    <body {% if request.is_mobile %}class="mobile"{% endif %}>
 | 
			
		||||
        <script id="init-script">
 | 
			
		||||
            window.addEventListener('load', function() {
 | 
			
		||||
                {% block init-scripts %}
 | 
			
		||||
@ -51,7 +48,7 @@ Usefull context:
 | 
			
		||||
            <div class="navs">
 | 
			
		||||
                {% block nav %}
 | 
			
		||||
                <nav class="nav primary" role="navigation" aria-label="main navigation">
 | 
			
		||||
                    {% block nav-primary %}
 | 
			
		||||
                    {% block primary-nav %}
 | 
			
		||||
                    <a class="nav-brand" href="{% url "home" %}">
 | 
			
		||||
                        <img src="{{ station.logo.url }}">
 | 
			
		||||
                    </a>
 | 
			
		||||
@ -60,7 +57,7 @@ Usefull context:
 | 
			
		||||
                        aria-label="{% translate "Main menu" %}">
 | 
			
		||||
                    </a-switch>
 | 
			
		||||
                    <div class="nav-menu">
 | 
			
		||||
                        {% block nav-primary-menu %}
 | 
			
		||||
                        {% block primary-nav-menu %}
 | 
			
		||||
                        {% nav_items "top" css_class="nav-item" active_class="active" as items %}
 | 
			
		||||
                        {% for item, render in items %}
 | 
			
		||||
                        {{ render }}
 | 
			
		||||
@ -70,9 +67,6 @@ Usefull context:
 | 
			
		||||
                            {% translate "Admin" %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        <a class="nav-item" href="{% url "profile" %}" target="new">
 | 
			
		||||
                            {% translate "Profile" %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                        {% if user.is_authenticated %}
 | 
			
		||||
                        <a class="nav-item" href="{% url "logout" %}" title="{% translate "Disconnect" %}"
 | 
			
		||||
                            aria-label="{% translate "Disconnect" %}">
 | 
			
		||||
@ -99,22 +93,23 @@ Usefull context:
 | 
			
		||||
                {% endblock %}
 | 
			
		||||
                {% endspaceless %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                {% block header-container %}
 | 
			
		||||
                {% if page or cover or title %}
 | 
			
		||||
                <header class="container header preview preview-header {% if cover %}has-cover{% endif %}">
 | 
			
		||||
                    {% block header %}
 | 
			
		||||
                    {% if cover %}
 | 
			
		||||
                    <figure class="header-cover">
 | 
			
		||||
                        <img src="{{ cover }}" class="cover">
 | 
			
		||||
                        {% block header-cover %}
 | 
			
		||||
                        {% if cover %}
 | 
			
		||||
                        <img src="{{ cover }}" ref="cover" class="cover">
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        {% endblock %}
 | 
			
		||||
                    </figure>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    <div class="headings preview-card-headings">
 | 
			
		||||
                    {% block headings %}
 | 
			
		||||
                    <div>
 | 
			
		||||
                        {% block title-container %}
 | 
			
		||||
                        <h1 class="title is-1 {% block title-class %}{% endblock %}">{% block title %}{{ title|default:"" }}{% endblock %}</h1>
 | 
			
		||||
                        {% include "aircox/edit-link.html" %}
 | 
			
		||||
                        {% endblock %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div>
 | 
			
		||||
                        {% spaceless %}
 | 
			
		||||
@ -147,6 +142,33 @@ Usefull context:
 | 
			
		||||
            {% endblock %}
 | 
			
		||||
            </main>
 | 
			
		||||
            {% endblock %}
 | 
			
		||||
 | 
			
		||||
            {% block footer-container %}
 | 
			
		||||
            <footer class="page-footer">
 | 
			
		||||
                {% block footer %}
 | 
			
		||||
                {% comment %}
 | 
			
		||||
                {% nav_items "footer" css_class="nav-item" active_class="active" as items %}
 | 
			
		||||
                {% for item, render in items %}
 | 
			
		||||
                {{ render }}
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
                {% endcomment %}
 | 
			
		||||
 | 
			
		||||
                {% if not request.user.is_authenticated %}
 | 
			
		||||
                <a class="nav-item" href="{% url "profile" %}" target="new"
 | 
			
		||||
                        title="{% translate "Profile" %}">
 | 
			
		||||
                    <span class="small icon">
 | 
			
		||||
                        <i class="fa fa-user"></i>
 | 
			
		||||
                    </span>
 | 
			
		||||
                </a>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% endblock %}
 | 
			
		||||
 | 
			
		||||
                {% if request.station and request.station.legal_label %}
 | 
			
		||||
                {{ request.station.legal_label }} —
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </footer>
 | 
			
		||||
            {% endblock %}
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
        {% block player-container %}
 | 
			
		||||
        <div id="player">{% include "aircox/widgets/player.html" %}</div>
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
 | 
			
		||||
{% block list-container %}
 | 
			
		||||
<section class="container clear-both list list-grid {{ list_class|default:"" }}" role="list">
 | 
			
		||||
<section class="container clear-both list grid {{ list_class|default:"" }}" role="list">
 | 
			
		||||
{% block list %}
 | 
			
		||||
{% with has_headline=True %}
 | 
			
		||||
    {% for object in object_list %}
 | 
			
		||||
 | 
			
		||||
@ -1,24 +0,0 @@
 | 
			
		||||
{% load aircox i18n %}
 | 
			
		||||
{% block user-actions-container %}
 | 
			
		||||
{% has_perm page page.program.change_permission_codename simple=True as can_edit %}
 | 
			
		||||
{% if user.is_authenticated and can_edit %}
 | 
			
		||||
{% with request.resolver_match.view_name as view_name %}
 | 
			
		||||
   
 | 
			
		||||
  {% if view_name in 'page-edit,program-edit,episode-edit' %}
 | 
			
		||||
  <a href="{% url view_name|detail_view page.slug %}" target="_self">
 | 
			
		||||
      <small>
 | 
			
		||||
      <span title="{% translate 'View' %} {{ page }}">{% translate 'View' %} </span>
 | 
			
		||||
      <i class="fa-regular fa-eye"></i>
 | 
			
		||||
      </small>
 | 
			
		||||
  </a>
 | 
			
		||||
  {% else %}
 | 
			
		||||
  <a href="{% url view_name|edit_view page.pk %}" target="_self">
 | 
			
		||||
      <small>
 | 
			
		||||
      <span title="{% translate 'Edit' %} {{ page }}">{% translate 'Edit' %} </span>
 | 
			
		||||
      <i class="fa-solid fa-pencil"></i>
 | 
			
		||||
      </small>
 | 
			
		||||
  </a>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -26,7 +26,6 @@
 | 
			
		||||
    </table>
 | 
			
		||||
    <br/>
 | 
			
		||||
    <input type="submit" value="Update" class="button is-success">
 | 
			
		||||
 | 
			
		||||
    <hr>
 | 
			
		||||
 | 
			
		||||
    {% include "aircox/playlist_inline.html" %}
 | 
			
		||||
 | 
			
		||||
@ -41,7 +41,7 @@
 | 
			
		||||
<section class="container">
 | 
			
		||||
    <h2 class="title">{% translate "It just happened" %}</h2>
 | 
			
		||||
 | 
			
		||||
    <div class="list-grid" role="list">
 | 
			
		||||
    <div class="grid" role="list">
 | 
			
		||||
        {% include "./widgets/logs.html" with object_list=logs %}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,12 @@ Context:
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block title-container %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
{% block page-actions %}
 | 
			
		||||
    {% include "aircox/widgets/page_actions.html" %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block main %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										71
									
								
								aircox/templates/aircox/page_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								aircox/templates/aircox/page_form.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
			
		||||
{% extends "./page_detail.html" %}
 | 
			
		||||
{% load static i18n %}
 | 
			
		||||
 | 
			
		||||
{% block header-cover %}
 | 
			
		||||
<img src="{{ cover }}" ref="cover" class="cover">
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content-container %}
 | 
			
		||||
<section class="container active">
 | 
			
		||||
<form method="post" enctype="multipart/form-data">
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    {% for field in form %}
 | 
			
		||||
    <div class="field">
 | 
			
		||||
        {% if field.name == "cover" %}
 | 
			
		||||
        <input type="hidden" name="{{ field.name }}" value="{{ field.pk }}" ref="coverField"/>
 | 
			
		||||
        {% else %}
 | 
			
		||||
        <label class="label">{{ field.label }}</label>
 | 
			
		||||
        <div class="control clear-unset">
 | 
			
		||||
            {% if field.name == "pub_date" %}
 | 
			
		||||
            <input type="datetime-local" name="{{ field.name }}"
 | 
			
		||||
                value="{{ field.value|date:"Y-m-d" }}T{{ field.value|date:"H:i" }}"/>
 | 
			
		||||
            {% elif field.name == "content" %}
 | 
			
		||||
            <textarea name="{{ field.name }}" class="is-fullwidth">{{ field.value|striptags|safe }}</textarea>
 | 
			
		||||
            {% else %}
 | 
			
		||||
            {{ field }}
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
    {% if field.errors %}
 | 
			
		||||
    <p class="help is-danger">{{ field.errors }}</p>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    {% if field.help_text %}
 | 
			
		||||
    <p class="help">{{ field.help_text|safe }}</p>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
 | 
			
		||||
    <div class="has-text-right">
 | 
			
		||||
        <button type="submit" class="button">{% translate "Update" %}</button>
 | 
			
		||||
    </div>
 | 
			
		||||
</form>
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
<section id="cover-modal" class="container page-edit-panel">
 | 
			
		||||
    <h3 class="title">{% translate "Change cover" %}</h3>
 | 
			
		||||
    <a-select-file list-url="{% url "api:image-list" %}" upload-url="{% url "api:image-list" %}"
 | 
			
		||||
        prev-label="{% translate "Show previous" %}"
 | 
			
		||||
        next-label="{% translate "Show next" %}"
 | 
			
		||||
        >
 | 
			
		||||
        <template #upload-preview="{item}">
 | 
			
		||||
            <template v-if="item">
 | 
			
		||||
                <img :src="item.src" class="upload-preview"/>
 | 
			
		||||
            </template>
 | 
			
		||||
        </template>
 | 
			
		||||
        <template #default="{item}">
 | 
			
		||||
            <div class="flex-column">
 | 
			
		||||
                <div class="flex-grow-1">
 | 
			
		||||
                    <img :src="item.file"/>
 | 
			
		||||
                </div>
 | 
			
		||||
                <label class="label">[[ item.name || item.original_filename ]]</label>
 | 
			
		||||
            </div>
 | 
			
		||||
        </template>
 | 
			
		||||
        <template #footer="{item}">
 | 
			
		||||
            <button type="button" class="button float-right"
 | 
			
		||||
                @click="(event) => {$refs.cover.src = item.file; $refs.coverField.value = item.id}">
 | 
			
		||||
                {% translate "Select" %}
 | 
			
		||||
            </button>
 | 
			
		||||
        </template>
 | 
			
		||||
    </a-select-file>
 | 
			
		||||
</section>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -18,7 +18,7 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block list-container %}
 | 
			
		||||
{% with list_class="list-grid" %}
 | 
			
		||||
{% with list_class="grid" %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ Context:
 | 
			
		||||
- url_label: label of url button
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
 | 
			
		||||
<a-carousel section-class="card-grid">
 | 
			
		||||
<a-carousel>
 | 
			
		||||
    {% for object in objects %}
 | 
			
		||||
    {% page_widget "card" object %}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										30
									
								
								aircox/templates/aircox/widgets/page_actions.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								aircox/templates/aircox/widgets/page_actions.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
{% load aircox i18n %}
 | 
			
		||||
{% block user-actions-container %}
 | 
			
		||||
{% has_perm page page.program.change_permission_codename simple=True as can_edit %}
 | 
			
		||||
 | 
			
		||||
{% if user.is_authenticated %}
 | 
			
		||||
{{ object.get_status_display }}
 | 
			
		||||
({{ object.pub_date|date:"d/m/Y H:i" }})
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% if user.is_authenticated and can_edit %}
 | 
			
		||||
{% with request.resolver_match.view_name as view_name %}
 | 
			
		||||
   
 | 
			
		||||
  {% if "-edit" in view_name %}
 | 
			
		||||
  <a href="{% url view_name|detail_view page.slug %}" target="_self" title="{% translate 'View' %} {{ page }}">
 | 
			
		||||
      <span class="icon">
 | 
			
		||||
          <i class="fa-regular fa-eye"></i>
 | 
			
		||||
      </span>
 | 
			
		||||
      <span>{% translate 'View' %} </span>
 | 
			
		||||
  </a>
 | 
			
		||||
  {% else %}
 | 
			
		||||
  <a href="{% url view_name|edit_view page.pk %}" target="_self" title="{% translate 'Edit' %} {{ page }}">
 | 
			
		||||
      <span class="icon">
 | 
			
		||||
          <i class="fa-solid fa-pencil"></i>
 | 
			
		||||
      </span>
 | 
			
		||||
      <span>{% translate 'Edit' %} </span>
 | 
			
		||||
  </a>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -30,7 +30,7 @@ The audio player
 | 
			
		||||
            </h4>
 | 
			
		||||
            <h4 v-else-if="current && current.data.type == 'track'"
 | 
			
		||||
                class="title" aria-description="{% translate "Track currently on air" %}">
 | 
			
		||||
                <span class="icon hg-color-2 mr-3">
 | 
			
		||||
                <span class="icon secondary-color mr-3">
 | 
			
		||||
                    <i class="fas fa-music"></i>
 | 
			
		||||
                </span>
 | 
			
		||||
                <span>[[ current.data.title ]]</span>
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ Context:
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
 | 
			
		||||
<span class="track">
 | 
			
		||||
    <span class="icon hg-color-2">
 | 
			
		||||
    <span class="icon secondary-color">
 | 
			
		||||
        <i class="fas fa-music"></i>
 | 
			
		||||
    </span>
 | 
			
		||||
    <label>
 | 
			
		||||
 | 
			
		||||
@ -59,6 +59,8 @@ def do_get_tracks(obj):
 | 
			
		||||
@register.simple_tag(name="has_perm", takes_context=True)
 | 
			
		||||
def do_has_perm(context, obj, perm, user=None, simple=False):
 | 
			
		||||
    """Return True if ``user.has_perm('[APP].[perm]_[MODEL]')``"""
 | 
			
		||||
    if not obj:
 | 
			
		||||
        return
 | 
			
		||||
    if user is None:
 | 
			
		||||
        user = context["request"].user
 | 
			
		||||
    if simple:
 | 
			
		||||
@ -102,6 +104,8 @@ def do_player_live_attr(context):
 | 
			
		||||
@register.simple_tag(name="nav_items", takes_context=True)
 | 
			
		||||
def do_nav_items(context, menu, **kwargs):
 | 
			
		||||
    """Render navigation items for the provided menu name."""
 | 
			
		||||
    if not getattr(context["request"], "station"):
 | 
			
		||||
        return []
 | 
			
		||||
    station, request = context["station"], context["request"]
 | 
			
		||||
    return [(item, item.render(request, **kwargs)) for item in station.navitem_set.filter(menu=menu)]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ from django.urls import include, path, register_converter
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from rest_framework.routers import DefaultRouter
 | 
			
		||||
 | 
			
		||||
from . import models, views, viewsets
 | 
			
		||||
from . import forms, models, views, viewsets
 | 
			
		||||
from .converters import DateConverter, PagePathConverter, WeekConverter
 | 
			
		||||
 | 
			
		||||
__all__ = ["api", "urls"]
 | 
			
		||||
@ -21,6 +21,7 @@ register_converter(WeekConverter, "week")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
router = DefaultRouter()
 | 
			
		||||
router.register("images", viewsets.ImageViewSet, basename="image")
 | 
			
		||||
router.register("sound", viewsets.SoundViewSet, basename="sound")
 | 
			
		||||
router.register("track", viewsets.TrackROViewSet, basename="track")
 | 
			
		||||
 | 
			
		||||
@ -132,6 +133,12 @@ urls = [
 | 
			
		||||
        views.errors.NoStationErrorView.as_view(),
 | 
			
		||||
        name="errors-no-station",
 | 
			
		||||
    ),
 | 
			
		||||
    path("gestion/", views.ProfileView.as_view(), name="profile"),
 | 
			
		||||
    # ---- backoffice
 | 
			
		||||
    path(_("edit/"), views.ProfileView.as_view(), name="profile"),
 | 
			
		||||
    path(
 | 
			
		||||
        _("edit/programs/<slug:slug>"),
 | 
			
		||||
        views.PageUpdateView.as_view(model=models.Program, form_class=forms.ProgramForm),
 | 
			
		||||
        name="program-update",
 | 
			
		||||
    ),
 | 
			
		||||
    path("accounts/profile/", views.ProfileView.as_view(), name="profile"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,5 @@
 | 
			
		||||
from . import admin, errors
 | 
			
		||||
from .article import ArticleDetailView, ArticleListView
 | 
			
		||||
from .base import BaseAPIView, BaseView
 | 
			
		||||
from .diffusion import DiffusionListView, TimeTableView
 | 
			
		||||
from .episode import EpisodeDetailView, EpisodeListView, PodcastListView, EpisodeUpdateView
 | 
			
		||||
from .home import HomeView
 | 
			
		||||
@ -10,6 +9,7 @@ from .page import (
 | 
			
		||||
    BasePageListView,
 | 
			
		||||
    PageDetailView,
 | 
			
		||||
    PageListView,
 | 
			
		||||
    PageUpdateView,
 | 
			
		||||
)
 | 
			
		||||
from .profile import ProfileView
 | 
			
		||||
from .program import (
 | 
			
		||||
@ -20,13 +20,12 @@ from .program import (
 | 
			
		||||
    ProgramUpdateView,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = (
 | 
			
		||||
    "admin",
 | 
			
		||||
    "errors",
 | 
			
		||||
    "ArticleDetailView",
 | 
			
		||||
    "ArticleListView",
 | 
			
		||||
    "BaseAPIView",
 | 
			
		||||
    "BaseView",
 | 
			
		||||
    "DiffusionListView",
 | 
			
		||||
    "TimeTableView",
 | 
			
		||||
    "EpisodeDetailView",
 | 
			
		||||
@ -39,6 +38,7 @@ __all__ = (
 | 
			
		||||
    "BasePageDetailView",
 | 
			
		||||
    "BasePageListView",
 | 
			
		||||
    "PageDetailView",
 | 
			
		||||
    "PageUpdateView",
 | 
			
		||||
    "PageListView",
 | 
			
		||||
    "ProfileView",
 | 
			
		||||
    "ProgramDetailView",
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,10 @@ class BasePageMixin:
 | 
			
		||||
    category = None
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return super().get_queryset().select_subclasses().published().select_related("cover")
 | 
			
		||||
        qs = super().get_queryset().select_subclasses().select_related("cover")
 | 
			
		||||
        if self.request.user.is_authenticated:
 | 
			
		||||
            return qs
 | 
			
		||||
        return qs.published()
 | 
			
		||||
 | 
			
		||||
    def get_category(self, page, **kwargs):
 | 
			
		||||
        if page:
 | 
			
		||||
@ -153,8 +156,8 @@ class PageDetailView(BasePageDetailView):
 | 
			
		||||
        return super().get_queryset().select_related("category")
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        if self.object.allow_comments and "comment_form" not in kwargs:
 | 
			
		||||
            kwargs["comment_form"] = CommentForm()
 | 
			
		||||
        if "comment_form" not in kwargs:
 | 
			
		||||
            kwargs["comment_form"] = self.get_comment_form()
 | 
			
		||||
        kwargs["comments"] = Comment.objects.filter(page=self.object).order_by("-date")
 | 
			
		||||
 | 
			
		||||
        if self.object.parent_subclass:
 | 
			
		||||
@ -167,6 +170,11 @@ class PageDetailView(BasePageDetailView):
 | 
			
		||||
            kwargs["related_objects"] = related
 | 
			
		||||
        return super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_comment_form(self):
 | 
			
		||||
        if self.object.allow_comments:
 | 
			
		||||
            return CommentForm()
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def as_view(cls, *args, **kwargs):
 | 
			
		||||
        view = super(PageDetailView, cls).as_view(*args, **kwargs)
 | 
			
		||||
@ -186,6 +194,14 @@ class PageDetailView(BasePageDetailView):
 | 
			
		||||
 | 
			
		||||
class PageUpdateView(BaseView, UpdateView):
 | 
			
		||||
    context_object_name = "page"
 | 
			
		||||
    template_name = "aircox/page_form.html"
 | 
			
		||||
 | 
			
		||||
    # FIXME: remove?
 | 
			
		||||
    def get_page(self):
 | 
			
		||||
        return self.object
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return self.request.path
 | 
			
		||||
 | 
			
		||||
    def get_comment_form(self):
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
@ -56,9 +56,6 @@ class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
 | 
			
		||||
    model = Program
 | 
			
		||||
    form_class = ProgramForm
 | 
			
		||||
 | 
			
		||||
    def get_sidebar_queryset(self):
 | 
			
		||||
        return super().get_sidebar_queryset().filter(parent=self.program)
 | 
			
		||||
 | 
			
		||||
    def test_func(self):
 | 
			
		||||
        program = self.get_object()
 | 
			
		||||
        return self.request.user.has_perm("aircox.%s" % program.change_permission_codename)
 | 
			
		||||
@ -94,7 +91,3 @@ class ProgramPageListView(BaseProgramMixin, PageListView):
 | 
			
		||||
 | 
			
		||||
    def get_program(self):
 | 
			
		||||
        return self.parent
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        kwargs.setdefault("sidebar_url_parent", None)
 | 
			
		||||
        return super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
@ -1,44 +1,46 @@
 | 
			
		||||
from django_filters import rest_framework as filters
 | 
			
		||||
from django_filters import rest_framework as drf_filters
 | 
			
		||||
from rest_framework import status, viewsets
 | 
			
		||||
from rest_framework.decorators import action
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.parsers import MultiPartParser
 | 
			
		||||
 | 
			
		||||
from .models import Sound, Track
 | 
			
		||||
from filer.models.imagemodels import Image
 | 
			
		||||
 | 
			
		||||
from . import models, forms, filters
 | 
			
		||||
from .serializers import SoundSerializer, admin
 | 
			
		||||
from .views import BaseAPIView
 | 
			
		||||
 | 
			
		||||
__all__ = (
 | 
			
		||||
    "SoundFilter",
 | 
			
		||||
    "ImageViewSet",
 | 
			
		||||
    "SoundViewSet",
 | 
			
		||||
    "TrackFilter",
 | 
			
		||||
    "TrackROViewSet",
 | 
			
		||||
    "UserSettingsViewSet",
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SoundFilter(filters.FilterSet):
 | 
			
		||||
    station = filters.NumberFilter(field_name="program__station__id")
 | 
			
		||||
    program = filters.NumberFilter(field_name="program_id")
 | 
			
		||||
    episode = filters.NumberFilter(field_name="episode_id")
 | 
			
		||||
    search = filters.CharFilter(field_name="search", method="search_filter")
 | 
			
		||||
class ImageViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    parsers = (MultiPartParser,)
 | 
			
		||||
    serializer_class = admin.ImageSerializer
 | 
			
		||||
    queryset = Image.objects.all().order_by("-uploaded_at")
 | 
			
		||||
    filter_backends = (drf_filters.DjangoFilterBackend,)
 | 
			
		||||
    filterset_class = filters.ImageFilterSet
 | 
			
		||||
 | 
			
		||||
    def search_filter(self, queryset, name, value):
 | 
			
		||||
        return queryset.search(value)
 | 
			
		||||
    def create(self, request, **kwargs):
 | 
			
		||||
        # FIXME: to be replaced by regular DRF
 | 
			
		||||
        form = forms.ImageForm(request.POST, request.FILES)
 | 
			
		||||
        if form.is_valid():
 | 
			
		||||
            file = form.cleaned_data["file"]
 | 
			
		||||
            Image.objects.create(original_filename=file.name, file=file)
 | 
			
		||||
            return Response({"status": "ok"})
 | 
			
		||||
        return Response({"status": "error", "errors": form.errors})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SoundViewSet(BaseAPIView, viewsets.ModelViewSet):
 | 
			
		||||
    serializer_class = SoundSerializer
 | 
			
		||||
    queryset = Sound.objects.available().order_by("-pk")
 | 
			
		||||
    filter_backends = (filters.DjangoFilterBackend,)
 | 
			
		||||
    filterset_class = SoundFilter
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# --- admin
 | 
			
		||||
class TrackFilter(filters.FilterSet):
 | 
			
		||||
    artist = filters.CharFilter(field_name="artist", lookup_expr="icontains")
 | 
			
		||||
    album = filters.CharFilter(field_name="album", lookup_expr="icontains")
 | 
			
		||||
    title = filters.CharFilter(field_name="title", lookup_expr="icontains")
 | 
			
		||||
    queryset = models.Sound.objects.available().order_by("-pk")
 | 
			
		||||
    filter_backends = (drf_filters.DjangoFilterBackend,)
 | 
			
		||||
    filterset_class = filters.SoundFilterSet
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
 | 
			
		||||
@ -46,9 +48,9 @@ class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
 | 
			
		||||
 | 
			
		||||
    serializer_class = admin.TrackSerializer
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
    filter_backends = (filters.DjangoFilterBackend,)
 | 
			
		||||
    filterset_class = TrackFilter
 | 
			
		||||
    queryset = Track.objects.all()
 | 
			
		||||
    filter_backends = (drf_filters.DjangoFilterBackend,)
 | 
			
		||||
    filterset_class = filters.TrackFilterSet
 | 
			
		||||
    queryset = models.Track.objects.all()
 | 
			
		||||
 | 
			
		||||
    @action(name="autocomplete", detail=False)
 | 
			
		||||
    def autocomplete(self, request):
 | 
			
		||||
@ -60,6 +62,7 @@ class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
 | 
			
		||||
        return self.list(request)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# --- admin
 | 
			
		||||
class UserSettingsViewSet(viewsets.ViewSet):
 | 
			
		||||
    """User's settings specific to aircox.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -30,13 +30,6 @@ input.half-field:not(:active):not(:hover) {
 | 
			
		||||
    --link-fg: #00A6A6;
 | 
			
		||||
    --link-hv-fg: var(--text-color);
 | 
			
		||||
 | 
			
		||||
    --hg-color: #EFCA08;
 | 
			
		||||
    --hg-color-alpha: #EFCA08B3;
 | 
			
		||||
    --hg-color-grey: rgba(230, 230, 60, 1);
 | 
			
		||||
    --hg-color-2: #F49F0A;
 | 
			
		||||
    --hg-color-2-alpha: #F49F0AB3;
 | 
			
		||||
    --hg-color-2-grey: rgba(50, 200, 200, 1);
 | 
			
		||||
 | 
			
		||||
    --nav-primary-height: 3rem;
 | 
			
		||||
    --nav-secondary-height: 2.5rem;
 | 
			
		||||
    --nav-fg: var(--text-color);
 | 
			
		||||
@ -51,22 +44,38 @@ input.half-field:not(:active):not(:hover) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
    font-size: 1.4em;
 | 
			
		||||
    background-color: var(--body-bg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: v.$screen-wider) {
 | 
			
		||||
    body { font-size: 1.2em; }
 | 
			
		||||
@mixin mobile-small {
 | 
			
		||||
    .grid { @include grid-1; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
body.mobile {
 | 
			
		||||
    @include mobile-small;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: v.$screen-smaller) {
 | 
			
		||||
    @include mobile-small;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: v.$screen-normal) {
 | 
			
		||||
    body { font-size: 1em; }
 | 
			
		||||
    html { font-size: 18px !important; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    :root {
 | 
			
		||||
        --header-height: 20rem;
 | 
			
		||||
    }
 | 
			
		||||
@media screen and (max-width: v.$screen-wider) {
 | 
			
		||||
    html { font-size: 20px !important; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (min-width: v.$screen-wider) {
 | 
			
		||||
    html { font-size: 24px !important; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1, h2, h3, h4, h5, h6, .heading, .title, .subtitle {
 | 
			
		||||
@ -77,3 +86,8 @@ h1, h2, h3, h4, h5, h6, .heading, .title, .subtitle {
 | 
			
		||||
.container:empty {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.header-cover {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -31,8 +31,8 @@
 | 
			
		||||
    --preview-cover-size: 14rem;
 | 
			
		||||
    --preview-cover-small-size: 10rem;
 | 
			
		||||
    --preview-cover-tiny-size: 4rem;
 | 
			
		||||
    --preview-wide-content-sz: #{v.$text-size-bigger};
 | 
			
		||||
    --preview-heading-bg-color: var(--hg-color);
 | 
			
		||||
    --preview-wide-content-sz: #{v.$text-size-2};
 | 
			
		||||
    --preview-heading-bg-color: var(--main-color);
 | 
			
		||||
    --header-height: var(--cover-h);
 | 
			
		||||
 | 
			
		||||
    --a-carousel-p: #{v.$text-size-medium};
 | 
			
		||||
@ -89,9 +89,9 @@
 | 
			
		||||
 | 
			
		||||
        --section-content-sz: 1rem;
 | 
			
		||||
 | 
			
		||||
        --preview-title-sz: #{v.$text-size};
 | 
			
		||||
        --preview-subtitle-sz: #{v.$text-size-smaller};
 | 
			
		||||
        --preview-wide-content-sz: #{v.$text-size};
 | 
			
		||||
        // --preview-title-sz: #{v.$text-size};
 | 
			
		||||
        // --preview-subtitle-sz: #{v.$text-size-smaller};
 | 
			
		||||
        // --preview-wide-content-sz: #{v.$text-size};
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -138,6 +138,11 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// ---- panels
 | 
			
		||||
.panels {
 | 
			
		||||
    .panel:not(.active) { display: none; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---- button
 | 
			
		||||
@mixin button {
 | 
			
		||||
    .button, a.button, button.button {
 | 
			
		||||
@ -177,7 +182,6 @@
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.active:not(:hover) {
 | 
			
		||||
            // border-color: var(--hg-color-alpha);
 | 
			
		||||
            color: var(--button-active-fg);
 | 
			
		||||
            background-color: var(--button-active-bg);
 | 
			
		||||
        }
 | 
			
		||||
@ -187,9 +191,9 @@
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &[disabled], &.disabled {
 | 
			
		||||
            background-color: var(--hg-color-grey);
 | 
			
		||||
            color: var(--hg-color-2);
 | 
			
		||||
            border-color: var(--hg-color-2-alpha);
 | 
			
		||||
            background-color: var(--text-color-light);
 | 
			
		||||
            color: var(--secondary-color);
 | 
			
		||||
            border-color: var(--secondary-color-light);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .dropdown-trigger {
 | 
			
		||||
@ -414,7 +418,8 @@
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
        figure {
 | 
			
		||||
            box-shadow: 0em 0em 1.2em rgba(0, 0, 0, 0.4) !important;
 | 
			
		||||
            // box-shadow: 0em 0em 1.2em rgba(0, 0, 0, 0.4) !important;
 | 
			
		||||
            box-shadow: 0em 0em 1em rgba(0,0,0,0.2);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        a {
 | 
			
		||||
@ -429,10 +434,6 @@
 | 
			
		||||
            display: block !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .title {
 | 
			
		||||
            overflow: hidden;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .subtitle {
 | 
			
		||||
            font-size: v.$text-size-2;
 | 
			
		||||
        }
 | 
			
		||||
@ -443,7 +444,7 @@
 | 
			
		||||
        position: relative;
 | 
			
		||||
 | 
			
		||||
        figure {
 | 
			
		||||
            box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2);
 | 
			
		||||
            // box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2);
 | 
			
		||||
            height: var(--cover-h);
 | 
			
		||||
            width: var(--cover-w);
 | 
			
		||||
        }
 | 
			
		||||
@ -459,25 +460,6 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// ---- grid
 | 
			
		||||
.list-grid {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: 1fr 1fr;
 | 
			
		||||
    grid-auto-flow: dense;
 | 
			
		||||
    gap: v.$mp-4;
 | 
			
		||||
 | 
			
		||||
    // .grid-wide { grid-column: 1 / 3; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: v.$screen-smaller) {
 | 
			
		||||
    .list-grid {
 | 
			
		||||
        grid-template-columns: 1fr;
 | 
			
		||||
        // .grid-wide { grid-column: 1; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// ---- ---- Carousel
 | 
			
		||||
.a-carousel {
 | 
			
		||||
    .a-carousel-viewport {
 | 
			
		||||
@ -724,3 +706,40 @@
 | 
			
		||||
            overflow: hidden;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
/// ----------------
 | 
			
		||||
.a-select-file {
 | 
			
		||||
    > *:not(:last-child) {
 | 
			
		||||
        margin-bottom: v.$mp-3;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .upload-preview {
 | 
			
		||||
        max-width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .a-select-file-list {
 | 
			
		||||
        max-height: 30rem;
 | 
			
		||||
        overflow-y: auto;
 | 
			
		||||
        display: grid;
 | 
			
		||||
        grid-template-columns: 1fr 1fr 1fr 1fr;
 | 
			
		||||
        gap: v.$mp-3;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .file-preview {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
 | 
			
		||||
        &:hover {
 | 
			
		||||
            box-shadow: 0em 0em 1em rgba(0,0,0,0.2);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.active {
 | 
			
		||||
            box-shadow: 0em 0em 1em rgba(0,0,0,0.4);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        img {
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            max-height: 10rem;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,23 +1,46 @@
 | 
			
		||||
@use "./vars";
 | 
			
		||||
@use "./vars" as v;
 | 
			
		||||
 | 
			
		||||
.text-light { weight: 400; color: var(--text-color-light); }
 | 
			
		||||
 | 
			
		||||
// ---- layout
 | 
			
		||||
.align-left { text-align: left; justify-content: left; }
 | 
			
		||||
.align-right { text-align: right; justify-content: right; }
 | 
			
		||||
 | 
			
		||||
.clear-left { clear: left !important }
 | 
			
		||||
.clear-right { clear: right !important }
 | 
			
		||||
.clear-both { clear: both !important }
 | 
			
		||||
.clear-unset { clear: unset !important }
 | 
			
		||||
 | 
			
		||||
.d-inline { display: inline !important; }
 | 
			
		||||
.d-block { display: block !important; }
 | 
			
		||||
.d-inline-block { display: inline-block !important; }
 | 
			
		||||
 | 
			
		||||
.push-right, .flex-push-right { margin-left: auto !important; }
 | 
			
		||||
.push-bottom { margin-top: auto !important; }
 | 
			
		||||
.p-relative { position: relative !important }
 | 
			
		||||
.p-absolute { position: absolute !important }
 | 
			
		||||
.p-fixed { position: fixed !important }
 | 
			
		||||
.p-sticky { position: sticky !important }
 | 
			
		||||
.p-static { position: static !important }
 | 
			
		||||
 | 
			
		||||
.ws-nowrap { white-space: nowrap; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr }
 | 
			
		||||
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr }
 | 
			
		||||
// ---- grid
 | 
			
		||||
@mixin grid {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: 1fr 1fr;
 | 
			
		||||
    grid-auto-flow: dense;
 | 
			
		||||
    gap: v.$mp-4;
 | 
			
		||||
}
 | 
			
		||||
@mixin grid-1 { grid-template-columns: 1fr; }
 | 
			
		||||
@mixin grid-2 { grid-template-columns: 1fr 1fr; }
 | 
			
		||||
@mixin grid-3 { grid-template-columns: 1fr 1fr 1fr; }
 | 
			
		||||
 | 
			
		||||
.grid { @include grid; }
 | 
			
		||||
.grid-1 { @include grid; @include grid-1; }
 | 
			
		||||
.grid-2 { @include grid; @include grid-2; }
 | 
			
		||||
.grid-3 { @include grid; @include grid-3; }
 | 
			
		||||
 | 
			
		||||
// ---- flex
 | 
			
		||||
.flex-row { display: flex; flex-direction: row }
 | 
			
		||||
.flex-column { display: flex; flex-direction: column }
 | 
			
		||||
.flex-grow-0 { flex-grow: 0 !important; }
 | 
			
		||||
@ -26,6 +49,7 @@
 | 
			
		||||
.float-right { float: right }
 | 
			
		||||
.float-left { float: left }
 | 
			
		||||
 | 
			
		||||
// ---- boxing
 | 
			
		||||
.is-fullwidth { width: 100%; }
 | 
			
		||||
.is-fullheight { height: 100%; }
 | 
			
		||||
.is-fixed-bottom {
 | 
			
		||||
@ -34,23 +58,13 @@
 | 
			
		||||
    margin-bottom: 0px;
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
}
 | 
			
		||||
.is-borderless { border: none; }
 | 
			
		||||
.no-border { border: 0px !important; }
 | 
			
		||||
 | 
			
		||||
.overflow-hidden { overflow: hidden }
 | 
			
		||||
.overflow-hidden.is-fullwidth { max-width: 100%; }
 | 
			
		||||
 | 
			
		||||
.p-relative { position: relative !important }
 | 
			
		||||
.p-absolute { position: absolute !important }
 | 
			
		||||
.p-fixed { position: fixed !important }
 | 
			
		||||
.p-sticky { position: sticky !important }
 | 
			
		||||
.p-static { position: static !important }
 | 
			
		||||
 | 
			
		||||
.height-full { height: 100%; }
 | 
			
		||||
 | 
			
		||||
.ws-nowrap { white-space: nowrap; }
 | 
			
		||||
.no-border { border: 0px !important; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
*[draggable="true"] {
 | 
			
		||||
    cursor: move;
 | 
			
		||||
}
 | 
			
		||||
@ -67,16 +81,16 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// -- colors
 | 
			
		||||
.hg-color { color: var(--highlight-color); }
 | 
			
		||||
.hg-color-2 { color: var(--highlight-color-2); }
 | 
			
		||||
.main-color { color: var(--main-color); }
 | 
			
		||||
.secondary-color { color: var(--secondary-color); }
 | 
			
		||||
 | 
			
		||||
.bg-transparent { background-color: transparent; }
 | 
			
		||||
 | 
			
		||||
.is-success {
 | 
			
		||||
    background-color: vars.$green !important;
 | 
			
		||||
    border-color: vars.$green-dark !important;
 | 
			
		||||
    background-color: v.$green !important;
 | 
			
		||||
    border-color: v.$green-dark !important;
 | 
			
		||||
}
 | 
			
		||||
.is-danger {
 | 
			
		||||
    background-color: vars.$red !important;
 | 
			
		||||
    border-color: vars.$red-dark !important;
 | 
			
		||||
    background-color: v.$red !important;
 | 
			
		||||
    border-color: v.$red-dark !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// ---- main theme & layout
 | 
			
		||||
 | 
			
		||||
.page {
 | 
			
		||||
    padding-bottom: 5rem;
 | 
			
		||||
 | 
			
		||||
@ -47,7 +48,7 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vc-weekday-1, .vc-weekday-7 {
 | 
			
		||||
    color: var(--hg-color-2) !important;
 | 
			
		||||
    color: var(--secondary-color) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -124,19 +125,15 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar-item.active, .table tr.is-selected {
 | 
			
		||||
    color: var(--hg-color-2);
 | 
			
		||||
    background-color: var(--hg-color);
 | 
			
		||||
    color: var(--secondary-color);
 | 
			
		||||
    background-color: var(--main-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// -- headings
 | 
			
		||||
.title {
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
 | 
			
		||||
    &.is-3 {
 | 
			
		||||
        margin-top: v.$mp-3;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.is-3 { margin-top: v.$mp-3; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -467,12 +464,6 @@ nav li {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// ---- responsive
 | 
			
		||||
body { font-size: 1.4em; }
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: v.$screen-wide) {
 | 
			
		||||
    body { font-size: 1em; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: v.$screen-normal) {
 | 
			
		||||
    .page .container {
 | 
			
		||||
        margin-left: v.$mp-4;
 | 
			
		||||
@ -485,5 +476,4 @@ body { font-size: 1.4em; }
 | 
			
		||||
        margin-left: v.$mp-2;
 | 
			
		||||
        margin-right: v.$mp-2;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,7 @@ $title-color: #000;
 | 
			
		||||
@import "~bulma/sass/elements/box";
 | 
			
		||||
// @import "~bulma/sass/elements/button";
 | 
			
		||||
@import "~bulma/sass/elements/container";
 | 
			
		||||
@import "~bulma/sass/elements/content";
 | 
			
		||||
// @import "~bulma/sass/elements/content";
 | 
			
		||||
@import "~bulma/sass/elements/icon";
 | 
			
		||||
// @import "~bulma/sass/elements/image";
 | 
			
		||||
// @import "~bulma/sass/elements/notification";
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										124
									
								
								assets/src/components/ASelectFile.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								assets/src/components/ASelectFile.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,124 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="a-select-file">
 | 
			
		||||
        <div class="a-select-file-list" ref="list">
 | 
			
		||||
            <div class="flex-column file-preview">
 | 
			
		||||
                <div class="field flex-grow-1" v-if="!uploadFile">
 | 
			
		||||
                    <label class="label">{{ uploadLabel }}</label>
 | 
			
		||||
                    <input type="file" @change="previewFile"/>
 | 
			
		||||
                </div>
 | 
			
		||||
                <slot name="upload-preview" :item="uploadFile"></slot>
 | 
			
		||||
                <div v-if="uploadFile">
 | 
			
		||||
                    <button class="button secondary" @click="removeUpload">
 | 
			
		||||
                        <span class="icon">
 | 
			
		||||
                            <i class="fa fa-trash"></i>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <button class="button float-right" @click="doUpload">
 | 
			
		||||
                        <span class="icon">
 | 
			
		||||
                            <i class="fa fa-upload"></i>
 | 
			
		||||
                        </span>
 | 
			
		||||
                        Upload
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div v-if="prevUrl">
 | 
			
		||||
                <a href="#" @click="load(prevUrl)">
 | 
			
		||||
                    {{ prevLabel }}
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <template v-for="item in items" v-bind:key="item.id">
 | 
			
		||||
                <div :class="['file-preview', this.item && item.id == this.item.id && 'active']" @click="select(item)">
 | 
			
		||||
                    <slot :item="item"></slot>
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <div v-if="nextUrl">
 | 
			
		||||
                <a href="#" @click="load(nextUrl)">
 | 
			
		||||
                    {{ nextLabel }}
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="a-select-footer">
 | 
			
		||||
            <slot name="footer" :item="item" :items="items"></slot>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
import {getCsrf} from "../model"
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    props: {
 | 
			
		||||
        name: { type: String },
 | 
			
		||||
        prevLabel: { type: String, default: "Prev" },
 | 
			
		||||
        nextLabel: { type: String, default: "Next" },
 | 
			
		||||
        listUrl: { type: String },
 | 
			
		||||
        uploadLabel: { type: String, default: "Upload a file" },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            item: null,
 | 
			
		||||
            items: [],
 | 
			
		||||
            uploadFile: null,
 | 
			
		||||
            uploadUrl: null,
 | 
			
		||||
            uploadFieldName: null,
 | 
			
		||||
            uploadCSRF: null,
 | 
			
		||||
            nextUrl: "",
 | 
			
		||||
            prevUrl: "",
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        previewFile(event) {
 | 
			
		||||
            const [file] = event.target.files
 | 
			
		||||
            this.uploadFile = file && {
 | 
			
		||||
                file: file,
 | 
			
		||||
                src: URL.createObjectURL(file)
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        removeUpload() {
 | 
			
		||||
            this.uploadFile = null;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        doUpload() {
 | 
			
		||||
            const formData = new FormData();
 | 
			
		||||
            formData.append('file', this.uploadFile.file)
 | 
			
		||||
            formData.append('original_filename', this.uploadFile.file.name)
 | 
			
		||||
            formData.append('csrfmiddlewaretoken', getCsrf())
 | 
			
		||||
            fetch(this.listUrl, {
 | 
			
		||||
                method: "POST",
 | 
			
		||||
                body: formData
 | 
			
		||||
            }).then(
 | 
			
		||||
                () => {
 | 
			
		||||
                    this.uploadFile = null;
 | 
			
		||||
                    this.load()
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        load(url) {
 | 
			
		||||
            fetch(url || this.listUrl).then(
 | 
			
		||||
                response => response.ok ? response.json() : Promise.reject(response)
 | 
			
		||||
            ).then(data => {
 | 
			
		||||
                this.nextUrl = data.next
 | 
			
		||||
                this.prevUrl = data.previous
 | 
			
		||||
                this.items = data.results
 | 
			
		||||
 | 
			
		||||
                this.$forceUpdate()
 | 
			
		||||
                this.$refs.list.scroll(0, 0)
 | 
			
		||||
            })
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        select(item) {
 | 
			
		||||
            this.item = item;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.load()
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <button :title="ariaLabel"
 | 
			
		||||
            type="button"
 | 
			
		||||
            :aria-label="ariaLabel || label" :aria-description="ariaDescription"
 | 
			
		||||
            @click="toggle" :class="buttonClass">
 | 
			
		||||
        <slot name="default" :active="active">
 | 
			
		||||
@ -49,17 +50,21 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        set(active) {
 | 
			
		||||
            const el = document.querySelector(this.el)
 | 
			
		||||
            if(active)
 | 
			
		||||
                el.classList.add(this.activeClass)
 | 
			
		||||
            else
 | 
			
		||||
                el.classList.remove(this.activeClass)
 | 
			
		||||
            if(this.el) {
 | 
			
		||||
                const el = document.querySelector(this.el)
 | 
			
		||||
                if(active)
 | 
			
		||||
                    el.classList.add(this.activeClass)
 | 
			
		||||
                else
 | 
			
		||||
                    el.classList.remove(this.activeClass)
 | 
			
		||||
            }
 | 
			
		||||
            this.active = active
 | 
			
		||||
            if(active)
 | 
			
		||||
                this.resetGroup()
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        resetGroup() {
 | 
			
		||||
            if(!this.groupClass)
 | 
			
		||||
                return
 | 
			
		||||
            const els = document.querySelectorAll("." + this.groupClass)
 | 
			
		||||
            for(var el of els)
 | 
			
		||||
                if(el != this.$el)
 | 
			
		||||
 | 
			
		||||
@ -12,13 +12,15 @@ import ASoundItem from './ASoundItem.vue'
 | 
			
		||||
import ASwitch from './ASwitch.vue'
 | 
			
		||||
import AStatistics from './AStatistics.vue'
 | 
			
		||||
import AStreamer from './AStreamer.vue'
 | 
			
		||||
import ASelectFile from "./ASelectFile.vue"
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Core components
 | 
			
		||||
 */
 | 
			
		||||
export const base = {
 | 
			
		||||
    AAutocomplete, ACarousel, ADropdown, AEpisode, AList, APage, APlayer, APlaylist,
 | 
			
		||||
    AProgress, ASoundItem, ASwitch
 | 
			
		||||
    AProgress, ASoundItem, ASwitch,
 | 
			
		||||
    ASelectFile,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default base
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,7 @@ export default class Live {
 | 
			
		||||
            response.ok ? response.json()
 | 
			
		||||
                        : Promise.reject(response)
 | 
			
		||||
        ).then(data => {
 | 
			
		||||
            data = data.results
 | 
			
		||||
            data.forEach(item => {
 | 
			
		||||
                if(item.start) item.start = new Date(item.start)
 | 
			
		||||
                if(item.end) item.end = new Date(item.end)
 | 
			
		||||
 | 
			
		||||
@ -252,3 +252,6 @@ TEMPLATES = [
 | 
			
		||||
WSGI_APPLICATION = "instance.wsgi.application"
 | 
			
		||||
 | 
			
		||||
LOGOUT_REDIRECT_URL = "/"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
REST_FRAMEWORK = {"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 50}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user