Compare commits
24 Commits
develop-1.
...
c09ad6c1fd
Author | SHA1 | Date | |
---|---|---|---|
c09ad6c1fd | |||
eb77652569 | |||
3caeab15d9 | |||
92b6bcfae5 | |||
8b55ab5dea | |||
c5ecca2d36 | |||
efac8997f2 | |||
3fa038ddf9 | |||
9a702202e2 | |||
4adacd1f80 | |||
acfd5c49b7 | |||
29b4dc2de5 | |||
4e1c876d62 | |||
86e0b1a7a0 | |||
6615ebe5da | |||
2513d9eff5 | |||
b7429e11f0 | |||
a0be3c0fda | |||
070af46ef1 | |||
a8719bbc80 | |||
1551e1310f | |||
f29cced5f5 | |||
a323901d0e | |||
0a7a615288 |
@ -37,3 +37,4 @@ class EpisodeAdmin(SortableAdminBase, ChildPageAdmin):
|
|||||||
# readonly_fields = ('parent',)
|
# readonly_fields = ('parent',)
|
||||||
|
|
||||||
inlines = (TrackInline, EpisodeSoundInline, DiffusionInline)
|
inlines = (TrackInline, EpisodeSoundInline, DiffusionInline)
|
||||||
|
ordering = ["-pub_date"]
|
||||||
|
@ -181,8 +181,8 @@ class Settings(BaseSettings):
|
|||||||
"""Allow comments."""
|
"""Allow comments."""
|
||||||
|
|
||||||
# ---- bleach
|
# ---- bleach
|
||||||
ALLOWED_TAGS = [*sanitizer.ALLOWED_TAGS, "br", "p", "h3", "h4", "h5"]
|
ALLOWED_TAGS = [*sanitizer.ALLOWED_TAGS, "br", "p", "hr", "h2", "h3", "h4", "h5", "iframe", "pre"]
|
||||||
ALLOWED_ATTRIBUTES = sanitizer.ALLOWED_ATTRIBUTES
|
ALLOWED_ATTRIBUTES = [*sanitizer.ALLOWED_ATTRIBUTES, "src", "width", "height", "frameborder", "href"]
|
||||||
ALLOWED_PROTOCOLS = sanitizer.ALLOWED_PROTOCOLS
|
ALLOWED_PROTOCOLS = sanitizer.ALLOWED_PROTOCOLS
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ Usefull context:
|
|||||||
<meta name="description" content="{{ site.description }}" />
|
<meta name="description" content="{{ site.description }}" />
|
||||||
<meta name="keywords" content="{{ site.tags }}" />
|
<meta name="keywords" content="{{ site.tags }}" />
|
||||||
<meta name="generator" content="Aircox" />
|
<meta name="generator" content="Aircox" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="{% thumbnail site.favicon 32x32 crop %}" />
|
<link rel="icon" href="{% thumbnail site.favicon 32x32 crop %}" />
|
||||||
|
|
||||||
{% block assets %}
|
{% block assets %}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<div class="dropdown-trigger">
|
<div class="dropdown-trigger">
|
||||||
<button class="button square" aria-haspopup="true" aria-controls="dropdown-menu" type="button">
|
<button class="button square" aria-haspopup="true" aria-controls="dropdown-menu" type="button">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fa fa-user" aria-hidden="true"></i>
|
<i class="fa-regular fa-user" aria-hidden="true" style="opacity: 0.6"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -40,11 +40,16 @@
|
|||||||
{% translate "Statistics" %}
|
{% translate "Statistics" %}
|
||||||
</a>
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<hr class="dropdown-divider" />
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="dropdown-item" href="{% url "logout" %}" data-force-reload="1">
|
{% if user.is_authenticated %}
|
||||||
{% translate "Disconnect" %}
|
<hr class="dropdown-divider" />
|
||||||
</a>
|
<form id="logout" action="{% url 'logout' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<a class="dropdown-item" href="#" type="submit" onclick="document.getElementById('logout').submit();">
|
||||||
|
{% translate "Disconnect" %}
|
||||||
|
</a>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -181,3 +181,8 @@ def is_checkbox(field):
|
|||||||
def is_select(field):
|
def is_select(field):
|
||||||
"""Return True if field is a select."""
|
"""Return True if field is a select."""
|
||||||
return isinstance(field.widget, forms.Select)
|
return isinstance(field.widget, forms.Select)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def model_name(instance):
|
||||||
|
return instance.__class__.__name__
|
||||||
|
136
radiocampus/aircox_urls.py
Executable file
@ -0,0 +1,136 @@
|
|||||||
|
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 .converters import DateConverter, PagePathConverter, WeekConverter
|
||||||
|
|
||||||
|
__all__ = ["api", "urls"]
|
||||||
|
|
||||||
|
|
||||||
|
register_converter(PagePathConverter, "page_path")
|
||||||
|
register_converter(DateConverter, "date")
|
||||||
|
register_converter(WeekConverter, "week")
|
||||||
|
|
||||||
|
|
||||||
|
# urls = [
|
||||||
|
# path('on_air', views.on_air, name='aircox.on_air'),
|
||||||
|
# path('monitor', views.Monitor.as_view(), name='aircox.monitor'),
|
||||||
|
# path('stats', views.StatisticsView.as_view(), name='aircox.stats'),
|
||||||
|
# ]
|
||||||
|
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register("user", viewsets.UserViewSet, basename="user")
|
||||||
|
router.register("group", viewsets.GroupViewSet, basename="group")
|
||||||
|
router.register("usergroup", viewsets.UserGroupViewSet, basename="usergroup")
|
||||||
|
|
||||||
|
router.register("images", viewsets.ImageViewSet, basename="image")
|
||||||
|
router.register("sound", viewsets.SoundViewSet, basename="sound")
|
||||||
|
router.register("track", viewsets.TrackROViewSet, basename="track")
|
||||||
|
router.register("comment", viewsets.CommentViewSet, basename="comment")
|
||||||
|
|
||||||
|
|
||||||
|
api = [
|
||||||
|
path("logs/", views.log.LogListAPIView.as_view(), name="live"),
|
||||||
|
path(
|
||||||
|
"user/settings/",
|
||||||
|
viewsets.UserSettingsViewSet.as_view({"get": "retrieve", "post": "update", "put": "update"}),
|
||||||
|
name="user-settings",
|
||||||
|
),
|
||||||
|
] + router.urls
|
||||||
|
|
||||||
|
|
||||||
|
urls = [
|
||||||
|
path("", views.home.HomeView.as_view(), name="home"),
|
||||||
|
path("api/", include((api, "aircox"), namespace="api")),
|
||||||
|
# ---- ---- objects views
|
||||||
|
# ---- articles
|
||||||
|
path(
|
||||||
|
_("articles/<slug:slug>/"),
|
||||||
|
views.article.ArticleDetailView.as_view(),
|
||||||
|
name="article-detail",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
_("articles/"),
|
||||||
|
views.article.ArticleListView.as_view(model=models.article.Article),
|
||||||
|
name="article-list",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
_("articles/c/<slug:category_slug>/"),
|
||||||
|
views.article.ArticleListView.as_view(model=models.article.Article),
|
||||||
|
name="article-list",
|
||||||
|
),
|
||||||
|
# ---- timetable
|
||||||
|
path(_("timetable/"), views.diffusion.TimeTableView.as_view(), name="timetable-list"),
|
||||||
|
path(
|
||||||
|
_("timetable/<date:date>/"),
|
||||||
|
views.diffusion.TimeTableView.as_view(),
|
||||||
|
name="timetable-list",
|
||||||
|
),
|
||||||
|
# ---- pages
|
||||||
|
path(
|
||||||
|
_("publications/"),
|
||||||
|
views.PageListView.as_view(model=models.Page, attach_to_value=models.StaticPage.Target.PAGES),
|
||||||
|
name="page-list",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
_("publications/c/<slug:category_slug>/"),
|
||||||
|
views.PageListView.as_view(model=models.Page, attach_to_value=models.StaticPage.Target.PAGES),
|
||||||
|
name="page-list",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
_("pages/<slug:slug>/"),
|
||||||
|
views.BasePageDetailView.as_view(
|
||||||
|
model=models.StaticPage,
|
||||||
|
queryset=models.StaticPage.objects.filter(attach_to__isnull=True),
|
||||||
|
),
|
||||||
|
name="static-page-detail",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
_("pages/"),
|
||||||
|
views.BasePageListView.as_view(
|
||||||
|
model=models.StaticPage,
|
||||||
|
queryset=models.StaticPage.objects.filter(attach_to__isnull=True),
|
||||||
|
),
|
||||||
|
name="static-page-list",
|
||||||
|
),
|
||||||
|
# ---- programs
|
||||||
|
path(_("programs/"), views.program.ProgramListView.as_view(), name="program-list"),
|
||||||
|
path(_("programs/c/<slug:category_slug>/"), views.program.ProgramListView.as_view(), name="program-list"),
|
||||||
|
path(
|
||||||
|
_("programs/<slug:slug>/"),
|
||||||
|
views.program.ProgramDetailView.as_view(),
|
||||||
|
name="program-detail",
|
||||||
|
),
|
||||||
|
path(_("programs/<slug:parent_slug>/articles/"), views.article.ArticleListView.as_view(), name="article-list"),
|
||||||
|
path(_("programs/<slug:parent_slug>/podcasts/"), views.episode.PodcastListView.as_view(), name="podcast-list"),
|
||||||
|
path(_("programs/<slug:parent_slug>/episodes/"), views.episode.EpisodeListView.as_view(), name="episode-list"),
|
||||||
|
path(
|
||||||
|
_("programs/<slug:parent_slug>/diffusions/"), views.diffusion.DiffusionListView.as_view(), name="diffusion-list"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
_("programs/<slug:parent_slug>/publications/"),
|
||||||
|
views.PageListView.as_view(model=models.Page, attach_to_value=models.StaticPage.Target.PAGES),
|
||||||
|
name="page-list",
|
||||||
|
),
|
||||||
|
# ---- episodes
|
||||||
|
path(_("programs/episodes/"), views.episode.EpisodeListView.as_view(), name="episode-list"),
|
||||||
|
path(_("programs/episodes/c/<slug:category_slug>/"), views.episode.EpisodeListView.as_view(), name="episode-list"),
|
||||||
|
path(
|
||||||
|
_("programs/episodes/<slug:slug>/"),
|
||||||
|
views.episode.EpisodeDetailView.as_view(),
|
||||||
|
name="episode-detail",
|
||||||
|
),
|
||||||
|
path(_("podcasts/"), views.episode.PodcastListView.as_view(), name="podcast-list"),
|
||||||
|
path(_("podcasts/c/<slug:category_slug>/"), views.episode.PodcastListView.as_view(), name="podcast-list"),
|
||||||
|
# ---- dashboard
|
||||||
|
path(_("dashboard/"), views.dashboard.DashboardView.as_view(), name="dashboard"),
|
||||||
|
path(_("dashboard/program/<pk>/"), views.program.ProgramUpdateView.as_view(), name="program-edit"),
|
||||||
|
path(_("dashboard/episodes/<pk>/"), views.episode.EpisodeUpdateView.as_view(), name="episode-edit"),
|
||||||
|
path(_("dashboard/statistics/"), views.dashboard.StatisticsView.as_view(), name="dashboard-statistics"),
|
||||||
|
path(_("dashboard/statistics/<date:date>/"), views.dashboard.StatisticsView.as_view(), name="dashboard-statistics"),
|
||||||
|
path(_("dashboard/users/"), views.auth.UserListView.as_view(), name="user-list"),
|
||||||
|
# ---- others
|
||||||
|
path(_("errors/no-station/"), views.errors.NoStationErrorView.as_view(), name="errors-no-station"),
|
||||||
|
]
|
29
radiocampus/assets/README.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# aircox
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
8
radiocampus/assets/jsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
4324
radiocampus/assets/package-lock.json
generated
Normal file
56
radiocampus/assets/package.json
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "aircox",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"watch": "vite build --watch",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^6.0.0",
|
||||||
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"@rollup/plugin-commonjs": "^25.0.7",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"v-calendar": "^3.1.2",
|
||||||
|
"vite-plugin-babel-macros": "^1.0.6",
|
||||||
|
"vue": "^3.4.21"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tiptap/extension-link": "^2.3.0",
|
||||||
|
"@tiptap/extension-underline": "^2.3.0",
|
||||||
|
"@tiptap/pm": "^2.3.0",
|
||||||
|
"@tiptap/starter-kit": "^2.3.0",
|
||||||
|
"@tiptap/vue-3": "^2.3.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"bulma": "^0.9.4",
|
||||||
|
"eslint": "^7.32.0",
|
||||||
|
"eslint-plugin-vue": "^8.0.3",
|
||||||
|
"sass": "^1.49.9",
|
||||||
|
"vite": "^5.2.8"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"es2022": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/vue3-essential",
|
||||||
|
"eslint:recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "@babel/eslint-parser"
|
||||||
|
},
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead",
|
||||||
|
"not ie 11"
|
||||||
|
]
|
||||||
|
}
|
34
radiocampus/assets/src/admin.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import './styles/admin.scss'
|
||||||
|
import './index.js'
|
||||||
|
|
||||||
|
import App from './app';
|
||||||
|
import components from './components/admin.js'
|
||||||
|
|
||||||
|
const AdminApp = {
|
||||||
|
...App,
|
||||||
|
components: {...App.components, ...components},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
...super.data,
|
||||||
|
modalItem: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
...App.methods,
|
||||||
|
|
||||||
|
fileSelected(select, input, preview) {
|
||||||
|
const item = this.$refs[select].item
|
||||||
|
if(item) {
|
||||||
|
this.$refs[input].value = item.id
|
||||||
|
if(preview)
|
||||||
|
preview.src = item.file
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default AdminApp;
|
||||||
|
|
||||||
|
|
||||||
|
window.App = AdminApp
|
45
radiocampus/assets/src/app.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {Calendar, DatePicker} from 'v-calendar';
|
||||||
|
import components from './components'
|
||||||
|
|
||||||
|
const App = {
|
||||||
|
el: '#app',
|
||||||
|
delimiters: ['[[', ']]'],
|
||||||
|
components: {
|
||||||
|
...components,
|
||||||
|
...{
|
||||||
|
VCalendar: Calendar,
|
||||||
|
VDatepicker: DatePicker
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
player() { return window.aircox.player; },
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
//! Delete elements from DOM using provided selector.
|
||||||
|
deleteElements(sel) {
|
||||||
|
for(var el of document.querySelectorAll(sel))
|
||||||
|
el.parentNode.removeChild(el)
|
||||||
|
},
|
||||||
|
|
||||||
|
//! File has been selected
|
||||||
|
//! TODO: replace using regular ref and bindings.
|
||||||
|
fileSelected(select, input, preview) {
|
||||||
|
const item = this.$refs[select].item
|
||||||
|
if(item) {
|
||||||
|
this.$refs[input].value = item.id
|
||||||
|
if(preview)
|
||||||
|
preview.src = item.file
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlayerApp = {
|
||||||
|
el: '#player',
|
||||||
|
delimiters: ['[[', ']]'],
|
||||||
|
components: {...components},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
27
radiocampus/assets/src/backgroundLoad.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Enable styling body background while using vue hotreload
|
||||||
|
// Tags with side effect (<script> and <style>) are ignored in client component templates.
|
||||||
|
|
||||||
|
const backgrounds = new Map();
|
||||||
|
backgrounds.set('default', "linear-gradient(#738ef2, white)");
|
||||||
|
backgrounds.set('/', "url(/static/radiocampus/backgrounds/photo-04-20.jpg) no-repeat center center fixed");
|
||||||
|
|
||||||
|
|
||||||
|
export default class BackgroundLoad {
|
||||||
|
constructor () {
|
||||||
|
let url = new URL(document.location)
|
||||||
|
this.path = url.pathname
|
||||||
|
this.update()
|
||||||
|
document.addEventListener("pageLoaded", this.handlePageLoad.bind(this), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePageLoad (e) {
|
||||||
|
this.path = e.detail
|
||||||
|
this.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
update () {
|
||||||
|
let background = backgrounds.get(this.path) || backgrounds.get("default")
|
||||||
|
document.body.style.background = background;
|
||||||
|
document.body.style.backgroundSize = "cover";
|
||||||
|
}
|
||||||
|
}
|
83
radiocampus/assets/src/components/AActionButton.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="tag" @click.capture.stop="call" type="button" :class="[buttonClass, this.promise && 'blink' || '']">
|
||||||
|
<span v-if="promise && runIcon">
|
||||||
|
<i :class="runIcon"></i>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="icon" class="icon is-small">
|
||||||
|
<i :class="icon"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="$slots.default"><slot name="default"/></span>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Model from '../model'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button that can be used to call API requests on provided url
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
emit: ['start', 'done'],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
//! Component tag, by default, `button`
|
||||||
|
tag: { type: String, default: 'a'},
|
||||||
|
//! Button icon
|
||||||
|
icon: String,
|
||||||
|
//! Data or model instance to send
|
||||||
|
data: Object,
|
||||||
|
//! Action method, by default, `POST`
|
||||||
|
method: { type: String, default: 'POST'},
|
||||||
|
//! If provided open confirmation box before proceeding
|
||||||
|
confirm: { type: String, default: ''},
|
||||||
|
//! Action url
|
||||||
|
url: String,
|
||||||
|
//! Extra request options
|
||||||
|
fetchOptions: {type: Object, default: () => {return {}}},
|
||||||
|
//! Component class while action is running
|
||||||
|
runClass: String,
|
||||||
|
//! Icon class while action is running
|
||||||
|
runIcon: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
//! Input data as model instance
|
||||||
|
item() {
|
||||||
|
return this.data instanceof Model ? this.data
|
||||||
|
: new Model(this.data)
|
||||||
|
},
|
||||||
|
|
||||||
|
//! Computed button class
|
||||||
|
buttonClass() {
|
||||||
|
return this.promise ? this.runClass : ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
promise: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
call() {
|
||||||
|
if(this.promise || !this.url)
|
||||||
|
return
|
||||||
|
if(this.confirm && !confirm(this.confirm))
|
||||||
|
return
|
||||||
|
|
||||||
|
const options = Model.getOptions({
|
||||||
|
...this.fetchOptions,
|
||||||
|
method: this.method,
|
||||||
|
body: JSON.stringify(this.item.data),
|
||||||
|
})
|
||||||
|
this.promise = fetch(this.url, options).then(data => data.text()).then(data => {
|
||||||
|
data = data && JSON.parse(data) || null
|
||||||
|
this.promise = null;
|
||||||
|
this.$emit('done', data)
|
||||||
|
return data
|
||||||
|
}, data => { this.promise = null; return data })
|
||||||
|
return this.promise
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
293
radiocampus/assets/src/components/AAutocomplete.vue
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
<template>
|
||||||
|
<div class="control">
|
||||||
|
<input type="hidden" :name="name" :value="selectedValue"
|
||||||
|
@change="$emit('change', $event)"/>
|
||||||
|
<input type="text" ref="input" class="input is-fullwidth" :class="inputClass"
|
||||||
|
v-show="!button || !selected"
|
||||||
|
v-model="inputValue"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
@keydown.capture="onKeyDown"
|
||||||
|
@keyup="onKeyUp($event); $emit('keyup', $event)"
|
||||||
|
@keydown="$emit('keydown', $event)"
|
||||||
|
@keypress="$emit('keypress', $event)"
|
||||||
|
@focus="onInputFocus" @blur="onBlur" />
|
||||||
|
<a v-if="selected && button"
|
||||||
|
class="button is-normal is-fullwidth has-text-left is-inline-block overflow-hidden"
|
||||||
|
@click="select(-1, false, true)">
|
||||||
|
<span class="icon is-small ml-1">
|
||||||
|
<i class="fa fa-pen"></i>
|
||||||
|
</span>
|
||||||
|
<span class="is-inline-block" v-if="selected">
|
||||||
|
<slot name="button" :index="selectedIndex" :item="selected"
|
||||||
|
:value-field="valueField" :labelField="labelField">
|
||||||
|
{{ selectedLabel }}
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div :class="dropdownClass">
|
||||||
|
<div class="dropdown-menu is-fullwidth">
|
||||||
|
<div class="dropdown-content" style="overflow: hidden">
|
||||||
|
<span v-for="(item, index) in items" :key="item.id"
|
||||||
|
:data-autocomplete-index="index"
|
||||||
|
@click="select(index, false, false)"
|
||||||
|
:class="['dropdown-item', (index == this.cursor) ? 'is-active':'']"
|
||||||
|
tabindex="-1">
|
||||||
|
<slot name="item" :index="index" :item="item" :value-field="valueField"
|
||||||
|
:labelField="labelField">
|
||||||
|
{{ getValue(item, labelField) || item }}
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// import debounce from 'lodash/debounce'
|
||||||
|
import Model from '../model'
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emit: ['change', 'keypress', 'keydown', 'keyup', 'select', 'unselect',
|
||||||
|
'update:modelValue'],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
//! Search URL (where `${query}` is replaced by search term)
|
||||||
|
url: String,
|
||||||
|
//! Extra GET url parameters
|
||||||
|
urlParams: Object,
|
||||||
|
//! Items' model
|
||||||
|
model: Function,
|
||||||
|
//! Input tag class
|
||||||
|
inputClass: Array,
|
||||||
|
//! input text placeholder
|
||||||
|
placeholder: Object,
|
||||||
|
//! input form field name
|
||||||
|
name: String,
|
||||||
|
//! Field on items to use as label
|
||||||
|
labelField: String,
|
||||||
|
//! Field on selected item to get selectedValue from, if any
|
||||||
|
valueField: {type: String, default: null},
|
||||||
|
count: {type: Number, count: 10},
|
||||||
|
//! If true, show button when value has been selected
|
||||||
|
button: Boolean,
|
||||||
|
//! If true, value must come from a selection
|
||||||
|
mustExist: {type: Boolean, default: false},
|
||||||
|
//! Minimum input size before fetching
|
||||||
|
minFetchLength: {type: Number, default: 3},
|
||||||
|
modelValue: {default: ''},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
inputValue: this.modelValue || '',
|
||||||
|
query: '',
|
||||||
|
items: [],
|
||||||
|
selectedIndex: -1,
|
||||||
|
cursor: -1,
|
||||||
|
promise: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
modelValue(value) {
|
||||||
|
this.inputValue = value
|
||||||
|
},
|
||||||
|
|
||||||
|
inputValue(value, old) {
|
||||||
|
if(value != old && value != this.modelValue) {
|
||||||
|
this.$emit('update:modelValue', value)
|
||||||
|
this.$emit('change', {target: this.$refs.input})
|
||||||
|
}
|
||||||
|
if(this.selectedLabel != value)
|
||||||
|
this.selectedIndex = -1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
fullUrl() {
|
||||||
|
if(!this.urlParams)
|
||||||
|
return this.url
|
||||||
|
|
||||||
|
const url = new URL(this.url, window.location.origin)
|
||||||
|
const params = new URLSearchParams(url.searchParams)
|
||||||
|
|
||||||
|
for(var key in this.urlParams)
|
||||||
|
params.set(key, this.urlParams[key])
|
||||||
|
const join = this.url.indexOf("?") >= 0 ? "&" : "?"
|
||||||
|
url.search = params.toString()
|
||||||
|
return url.href
|
||||||
|
},
|
||||||
|
|
||||||
|
isFetching() { return !!this.promise },
|
||||||
|
|
||||||
|
selected() {
|
||||||
|
let index = this.selectedIndex
|
||||||
|
if(index<0)
|
||||||
|
return null
|
||||||
|
index = Math.min(index, this.items.length-1)
|
||||||
|
return this.items[index]
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedValue() {
|
||||||
|
let value = this.itemValue(this.selected)
|
||||||
|
if(!value && !this.mustExist)
|
||||||
|
value = this.inputValue
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedLabel() {
|
||||||
|
return this.itemLabel(this.selected)
|
||||||
|
},
|
||||||
|
|
||||||
|
dropdownClass() {
|
||||||
|
var active = this.cursor > -1 && this.items.length;
|
||||||
|
if(active && this.items.length == 1 &&
|
||||||
|
this.itemValue(this.items[0]) == this.inputValue)
|
||||||
|
active = false
|
||||||
|
return ['dropdown is-fullwidth', active ? 'is-active':'']
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
reset() {
|
||||||
|
this.inputValue = ""
|
||||||
|
this.selectedIndex = -1
|
||||||
|
this.items = []
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: move to utils/data
|
||||||
|
getValue(data, path=null) {
|
||||||
|
if(!data)
|
||||||
|
return null
|
||||||
|
if(!path)
|
||||||
|
return data
|
||||||
|
|
||||||
|
const paths = path.split('.')
|
||||||
|
for(const key of paths) {
|
||||||
|
if(key in data)
|
||||||
|
data = data[key]
|
||||||
|
else return null;
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
|
itemValue(item) {
|
||||||
|
return this.valueField ? this.getValue(item, this.valueField) : item;
|
||||||
|
},
|
||||||
|
|
||||||
|
itemLabel(item) {
|
||||||
|
return this.labelField ? this.getValue(item, this.labelField) : item;
|
||||||
|
},
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.cursor = -1;
|
||||||
|
this.selectedIndex = -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
move(index=-1, relative=false) {
|
||||||
|
if(relative)
|
||||||
|
index += this.cursor
|
||||||
|
this.cursor = Math.max(-1, Math.min(index, this.items.length-1))
|
||||||
|
},
|
||||||
|
|
||||||
|
select(index=-1, relative=false, active=null) {
|
||||||
|
if(relative)
|
||||||
|
index += this.selectedIndex
|
||||||
|
else if(index == this.selectedIndex)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.selectedIndex = Math.max(-1, Math.min(index, this.items.length-1))
|
||||||
|
if(index >= 0) {
|
||||||
|
this.inputValue = this.selectedLabel
|
||||||
|
this.$refs.input.focus()
|
||||||
|
}
|
||||||
|
if(this.selectedIndex < 0)
|
||||||
|
this.$emit('unselect')
|
||||||
|
else
|
||||||
|
this.$emit('select', index, this.selected, this.selectedValue)
|
||||||
|
|
||||||
|
if(active!==null)
|
||||||
|
active && this.move(0) || this.move(-1)
|
||||||
|
},
|
||||||
|
|
||||||
|
onInputFocus() {
|
||||||
|
this.cursor < 0 && this.move(0)
|
||||||
|
},
|
||||||
|
|
||||||
|
onBlur(event) {
|
||||||
|
if(!this.items.length)
|
||||||
|
return
|
||||||
|
|
||||||
|
var index = event.relatedTarget && Math.floor(event.relatedTarget.dataset.autocompleteIndex);
|
||||||
|
if(index !== undefined && index !== null)
|
||||||
|
this.select(index, false, false)
|
||||||
|
this.cursor = -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyDown(event) {
|
||||||
|
if(event.ctrlKey || event.altKey || event.metaKey)
|
||||||
|
return
|
||||||
|
switch(event.keyCode) {
|
||||||
|
case 13: this.select(this.cursor, false, false)
|
||||||
|
break
|
||||||
|
case 27: this.hide(); this.select()
|
||||||
|
break
|
||||||
|
case 38: this.move(-1, true)
|
||||||
|
break
|
||||||
|
case 40: this.move(1, true)
|
||||||
|
break
|
||||||
|
default: return
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyUp(event) {
|
||||||
|
if(event.ctrlKey || event.altKey || event.metaKey)
|
||||||
|
return
|
||||||
|
|
||||||
|
const value = event.target.value
|
||||||
|
if(value === this.query)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.inputValue = value;
|
||||||
|
if(!value)
|
||||||
|
return this.selected && this.select(-1)
|
||||||
|
if(!this.minFetchLength || value.length >= this.minFetchLength)
|
||||||
|
this.fetch(value)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetch(query) {
|
||||||
|
if(!query || this.promise)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.query = query
|
||||||
|
var url = this.fullUrl.replace('${query}', query).replace('%24%7Bquery%7D', query)
|
||||||
|
var promise = this.model ? this.model.fetch(url, {many:true})
|
||||||
|
: fetch(url, Model.getOptions()).then(d => d.json())
|
||||||
|
|
||||||
|
promise = promise.then(items => {
|
||||||
|
if(items.results)
|
||||||
|
items = items.results
|
||||||
|
this.items = items.filter((i) => i) || []
|
||||||
|
this.promise = null;
|
||||||
|
this.move(0)
|
||||||
|
return items
|
||||||
|
}, data => {this.promise = null; Promise.reject(data)})
|
||||||
|
this.promise = promise
|
||||||
|
return promise
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
const form = this.$el.closest('form')
|
||||||
|
form && form.addEventListener('reset', () => {
|
||||||
|
this.inputValue = this.value;
|
||||||
|
this.select(-1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
242
radiocampus/assets/src/components/ACarousel.vue
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
<template>
|
||||||
|
<section class="a-carousel">
|
||||||
|
<nav ref="viewport" class="a-carousel-viewport">
|
||||||
|
<section ref="container" :class="['a-carousel-container', containerClass]">
|
||||||
|
<slot name="default"></slot>
|
||||||
|
</section>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<nav class="a-carousel-bullets-container">
|
||||||
|
<span class="left">
|
||||||
|
<span class="icon bullet" @click="prev()" v-if="showPrev">
|
||||||
|
<i :class="leftButtonIcon"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<template v-if="bullets.length > 1">
|
||||||
|
<span class="icon bullet" v-bind:key="bullet" v-for="bullet of bullets" @click="select(bullet)">
|
||||||
|
<i v-if="bullet == index" class="fa fa-circle"></i>
|
||||||
|
<i v-else class="far fa-circle"></i>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span class="right">
|
||||||
|
<span class="icon bullet" @click="next()" v-if="showNext">
|
||||||
|
<i :class="rightButtonIcon"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<slot name="bullets-right" :v-bind="this"></slot>
|
||||||
|
</nav>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.a-carousel {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-carousel-viewport {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-carousel-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-carousel-container > * {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-carousel-bullets-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-carousel-bullets-container .bullet {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-carousel-bullets-container .left {
|
||||||
|
min-width: 2rem;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-carousel-bullets-container .right {
|
||||||
|
min-width: 2rem;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-carousel-bullets-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import {ref} from 'vue'
|
||||||
|
|
||||||
|
|
||||||
|
class Offset {
|
||||||
|
constructor(el, min=null, max=null) {
|
||||||
|
this.el = el
|
||||||
|
this.rect = el.getBoundingClientRect();
|
||||||
|
({min, max} = this.minmax(min, max))
|
||||||
|
this.min = min
|
||||||
|
this.max = max
|
||||||
|
this.size = max-min
|
||||||
|
}
|
||||||
|
|
||||||
|
minmax(min=null, max=null) {
|
||||||
|
min = min === null ? this.rect.left : min
|
||||||
|
max = max === null ? this.rect.right : max
|
||||||
|
return {min, max}
|
||||||
|
}
|
||||||
|
|
||||||
|
relative(to) {
|
||||||
|
return new Offset(this.el, this.min-to.min, this.max-to.min)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Card extends Offset {
|
||||||
|
constructor(el, index) {
|
||||||
|
super(el)
|
||||||
|
this.index = index
|
||||||
|
}
|
||||||
|
|
||||||
|
visible(viewportOffset) {
|
||||||
|
return viewportOffset.min <= this.min && viewportOffset.max >= this.max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
viewport: ref(null),
|
||||||
|
container: ref(null),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
cards: [],
|
||||||
|
index: 0,
|
||||||
|
refresh_: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
cardSelector: {type: String, default: ''},
|
||||||
|
containerClass: {type: String, default: ''},
|
||||||
|
buttonClass: {type: String, default: 'button'},
|
||||||
|
leftButtonIcon: {type: String, default: "fas fa-chevron-left"},
|
||||||
|
rightButtonIcon: {type: String, default: "fas fa-chevron-right"},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
card() { return this.cards()[this.index] },
|
||||||
|
|
||||||
|
showPrev() {
|
||||||
|
return this.index > 0
|
||||||
|
},
|
||||||
|
|
||||||
|
showNext() {
|
||||||
|
if(!this.cards || this.cards.length <= 1)
|
||||||
|
return false
|
||||||
|
|
||||||
|
let last = this.bullets[this.bullets.length-1]
|
||||||
|
return this.index != last
|
||||||
|
},
|
||||||
|
|
||||||
|
bullets() {
|
||||||
|
if(!this.cards || !this.$refs.viewport)
|
||||||
|
return []
|
||||||
|
|
||||||
|
let contOff = new Offset(this.$refs.container)
|
||||||
|
let viewMax = new Offset(this.$refs.viewport).size
|
||||||
|
let bullets = []
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
let max = viewMax
|
||||||
|
bullets.push(i)
|
||||||
|
while(i < this.cards.length) {
|
||||||
|
// skip until next view
|
||||||
|
for(; i < this.cards.length; i++) {
|
||||||
|
let card = this.cards[i].relative(contOff)
|
||||||
|
if(card.max > max) {
|
||||||
|
max = card.min + viewMax
|
||||||
|
bullets.push(i)
|
||||||
|
i++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bullets
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getCards() {
|
||||||
|
if(!this.$refs.container)
|
||||||
|
return []
|
||||||
|
let nodes = (!this.cardSelector) ?
|
||||||
|
[...this.$refs.container.children] :
|
||||||
|
[...this.$refs.container.querySelectorAll(this.cardSelector)]
|
||||||
|
return nodes.map((el, index) => new Card(el, index))
|
||||||
|
},
|
||||||
|
|
||||||
|
select(index, relative=false) {
|
||||||
|
if(relative)
|
||||||
|
index = this.index + index
|
||||||
|
|
||||||
|
index = Math.min(index, this.cards.length)
|
||||||
|
index = Math.max(index, 0)
|
||||||
|
let card = this.cards[index]
|
||||||
|
if(!card)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
card = new Card(card.el)
|
||||||
|
const cont = new Offset(this.$refs.container)
|
||||||
|
const rel = card.relative(cont)
|
||||||
|
this.$refs.container.style.marginLeft = `-${rel.min}px`
|
||||||
|
this.index = index;
|
||||||
|
return card.el
|
||||||
|
},
|
||||||
|
|
||||||
|
next() {
|
||||||
|
let n = this.bullets.indexOf(this.index)
|
||||||
|
let index = this.bullets[n+1]
|
||||||
|
this.select(index)
|
||||||
|
},
|
||||||
|
|
||||||
|
prev() {
|
||||||
|
let n = this.bullets.indexOf(this.index)
|
||||||
|
let index = this.bullets[n-1]
|
||||||
|
this.select(index)
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.cards = this.getCards()
|
||||||
|
this.select(this.index)
|
||||||
|
this.refresh_++
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.observers = [
|
||||||
|
new MutationObserver(() => this.refresh()),
|
||||||
|
new ResizeObserver(() => this.refresh())
|
||||||
|
]
|
||||||
|
this.observers[0].observe(this.$refs.container, {"childList": true})
|
||||||
|
this.observers[1].observe(this.$refs.container)
|
||||||
|
this.refresh()
|
||||||
|
},
|
||||||
|
|
||||||
|
unmounted() {
|
||||||
|
for(var observer of this.observers)
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
49
radiocampus/assets/src/components/ADropdown.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="tag" :class="[itemClass, active ? activeClass : '']">
|
||||||
|
<slot name="before-button" :toggle="toggle" :active="active"></slot>
|
||||||
|
<slot name="button" :toggle="toggle" :active="active">
|
||||||
|
<component :is="buttonTag" :class="buttonClass" @click="toggle()">
|
||||||
|
<span class="icon" v-if="labelIcon">
|
||||||
|
<i :class="labelIcon"></i>
|
||||||
|
</span>
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
<span class="icon">
|
||||||
|
<i v-if="!active" :class="buttonIcon"></i>
|
||||||
|
<i v-if="active" :class="buttonIconClose"></i>
|
||||||
|
</span>
|
||||||
|
</component>
|
||||||
|
</slot>
|
||||||
|
<div :class="contentClass" v-show="active">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
active: this.open,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
tag: {type: String, default: "div"},
|
||||||
|
label: {type: String, default: ""},
|
||||||
|
labelIcon: {type: String, default: ""},
|
||||||
|
buttonTag: {type: String, default: "button"},
|
||||||
|
activeClass: {type: String, default: "is-active"},
|
||||||
|
buttonClass: {type: String, default: "button"},
|
||||||
|
buttonIcon: { type: String, default:"fa fa-angle-down"},
|
||||||
|
buttonIconClose: { type: String, default:"fa fa-angle-up"},
|
||||||
|
contentClass: String,
|
||||||
|
open: {type: Boolean, default: false},
|
||||||
|
noButton: {type: Boolean, default: false},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggle() {
|
||||||
|
this.active = !this.active
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
132
radiocampus/assets/src/components/AEditor.vue
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<input ref="input" type="hidden" :name="name" :value="value"/>
|
||||||
|
<div class="">
|
||||||
|
<template v-for="group, index in menu" :key="index">
|
||||||
|
<div class="button-group d-inline-block mr-3">
|
||||||
|
<template v-for="info, index in group" :key="index">
|
||||||
|
<button type="button" class="button square smaller" :title="info.label" @click="edit(info.action, ...(info.args || []))">
|
||||||
|
<span class="icon"><i :class="info.icon"/></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="button-group d-inline-block">
|
||||||
|
<div class="dropdown is-hoverable">
|
||||||
|
<div class="dropdown-trigger">
|
||||||
|
<button type="button" class="button square smaller">
|
||||||
|
<span class="icon"><i class="fa fa-link"/></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu" style="min-width: 20rem; margin-top: -0.2rem;">
|
||||||
|
<div class="dropdown-content p-3">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Lien</label>
|
||||||
|
<div class="control">
|
||||||
|
<input ref="link-url" type="text" class="input" placeholder="lien"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="has-text-right">
|
||||||
|
<button type="button" class="button secondary"
|
||||||
|
@click="edit('setLink', {href:$refs['link-url'].value})">
|
||||||
|
Ajouter le lien
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="button square smaller" title="Remove link" @click="edit('unsetLink')">
|
||||||
|
<span class="icon"><i class="fa fa-link-slash"/></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<editor-content class="editor" v-if="editor" :editor="editor" />
|
||||||
|
</template>
|
||||||
|
<style>
|
||||||
|
.editor .tiptap {
|
||||||
|
border: 1px black solid;
|
||||||
|
padding: 0.3em;
|
||||||
|
}
|
||||||
|
.editor .tiptap ul, .editor .tiptap ol {
|
||||||
|
margin-left: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor .tiptap ul { list-style: disc }
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import Underline from '@tiptap/extension-underline'
|
||||||
|
import Link from '@tiptap/extension-link'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {EditorContent},
|
||||||
|
props: {
|
||||||
|
config: {type: Object, default: (() => {})},
|
||||||
|
//! Input field name.
|
||||||
|
name: String,
|
||||||
|
//! Initial input value
|
||||||
|
initial: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
editor: null,
|
||||||
|
menu: [
|
||||||
|
[
|
||||||
|
{label: "Bold", icon: "fa fa-bold", action: "toggleBold" },
|
||||||
|
{label: "Italic", icon: "fa fa-italic", action: "toggleItalic" },
|
||||||
|
{label: "Underline", icon: "fa fa-underline", action: "toggleUnderline" },
|
||||||
|
{label: "Strike", icon: "fa fa-strikethrough", action: "toggleStrike" },
|
||||||
|
],[
|
||||||
|
{label: "List", icon: "fa fa-list", action: "toggleBulletList" },
|
||||||
|
{label: "Ordered List", icon: "fa fa-list-ol", action: "toggleOrderedList" },
|
||||||
|
],[
|
||||||
|
{label: "Heading 1", icon: "fa fa-h", action: "setHeading", args: [{level:3}] },
|
||||||
|
{label: "Heading 2", icon: "fa fa-h smaller", action: "toggleHeading", args: [{level:4}] },
|
||||||
|
// {label: "Heading 3", icon: "fa fa-h small", action: "toggleHeading", args: [{level:5}] },
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
value() { return this.editor && this.editor.getHTML() },
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
chain(action, ...args) {
|
||||||
|
let chain = this.editor.chain().focus()
|
||||||
|
return chain[action](...args)
|
||||||
|
},
|
||||||
|
|
||||||
|
edit(action, ...args) {
|
||||||
|
this.chain(action, ...args).run()
|
||||||
|
},
|
||||||
|
|
||||||
|
setLink() {
|
||||||
|
this.edit("setLink", {href: this.$refs['link-url']})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.editor = new Editor({
|
||||||
|
content: this.initial || "",
|
||||||
|
injectCss: false,
|
||||||
|
extensions: [
|
||||||
|
StarterKit.configure({
|
||||||
|
heading: {
|
||||||
|
levels: [3, 4, 5]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Underline,
|
||||||
|
Link.configure({autolink: true}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
beforeUnmount() {
|
||||||
|
this.editor.destroy()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
19
radiocampus/assets/src/components/AEpisode.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<slot :page="page" :podcasts="podcasts"></slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {Set} from '../model.js';
|
||||||
|
import Sound from '../sound.js';
|
||||||
|
import APage from './APage.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: APage,
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
podcasts: new Set(Sound, {items:this.page.podcasts}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
110
radiocampus/assets/src/components/AFileUpload.vue
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="list" class="a-select-file-list">
|
||||||
|
<form ref="form" class="flex-column" v-if="state == STATE.DEFAULT">
|
||||||
|
<slot name="form"></slot>
|
||||||
|
<div class="field is-horizontal">
|
||||||
|
<label class="label">{{ label }}</label>
|
||||||
|
<input type="file" ref="uploadFile" :name="fieldName" @change="onFileChange"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row align-right" v-if="submitLabel">
|
||||||
|
<button type="button" class="button small" @click="submit">
|
||||||
|
{{ submitLabel }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="flex-column" v-else>
|
||||||
|
<slot name="preview" :fileUrl="fileUrl" :file="file" :loaded="loaded" :total="total"></slot>
|
||||||
|
<div class="flex-row">
|
||||||
|
<progress :max="total" :value="loaded"/>
|
||||||
|
<button type="button" class="button small square ml-2" @click="abort">
|
||||||
|
<span class="icon small">
|
||||||
|
<i class="fa fa-close"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import {getCsrf} from "../model.js"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emit: ["fileChange", "load", "abort", "error"],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
url: { type: String },
|
||||||
|
fieldName: { type: String, default: "file" },
|
||||||
|
label: { type: String, default: "Select a file" },
|
||||||
|
submitLabel: { type: String, default: "Upload" },
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
STATE: {
|
||||||
|
DEFAULT: 0,
|
||||||
|
UPLOADING: 1,
|
||||||
|
},
|
||||||
|
state: 0,
|
||||||
|
upload: {},
|
||||||
|
file: null,
|
||||||
|
fileUrl: null,
|
||||||
|
total: 0,
|
||||||
|
loaded: 0,
|
||||||
|
request: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
abort() {
|
||||||
|
this.request && this.request.abort()
|
||||||
|
},
|
||||||
|
|
||||||
|
onFileChange() {
|
||||||
|
const [file] = this.$refs.uploadFile.files
|
||||||
|
if(!file)
|
||||||
|
return
|
||||||
|
this._setUploadFile(file)
|
||||||
|
this.$emit("fileChange", {upload: this, file: this.file, fileUrl: this.fileUrl})
|
||||||
|
},
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
const req = new XMLHttpRequest()
|
||||||
|
req.open("POST", this.url)
|
||||||
|
req.upload.addEventListener("progress", (e) => this.onUploadProgress(e))
|
||||||
|
req.addEventListener("load", (e) => this.onUploadDone(e, 'load'))
|
||||||
|
req.addEventListener("abort", (e) => this.onUploadDone(e, 'abort'))
|
||||||
|
req.addEventListener("error", (e) => this.onUploadDone(e, 'error'))
|
||||||
|
|
||||||
|
const formData = new FormData(this.$refs.form);
|
||||||
|
formData.append('csrfmiddlewaretoken', getCsrf())
|
||||||
|
req.send(formData)
|
||||||
|
|
||||||
|
this._resetUpload(this.STATE.UPLOADING, false, req)
|
||||||
|
},
|
||||||
|
|
||||||
|
onUploadProgress(event) {
|
||||||
|
this.loaded = event.loaded
|
||||||
|
this.total = event.total
|
||||||
|
},
|
||||||
|
|
||||||
|
onUploadDone(event, eventName) {
|
||||||
|
this.$emit(eventName, event)
|
||||||
|
this._resetUpload(this.STATE.DEFAULT, true)
|
||||||
|
},
|
||||||
|
|
||||||
|
_setUploadFile(file) {
|
||||||
|
this.file = file
|
||||||
|
this.fileURL = file && URL.createObjectURL(file)
|
||||||
|
},
|
||||||
|
|
||||||
|
_resetUpload(state, resetFile=false, request=null) {
|
||||||
|
this.state = state
|
||||||
|
this.loaded = 0
|
||||||
|
this.total = 0
|
||||||
|
this.request = request
|
||||||
|
if(resetFile)
|
||||||
|
this.file = null
|
||||||
|
}
|
||||||
|
|
||||||
|
},}
|
||||||
|
</script>
|
193
radiocampus/assets/src/components/AFormSet.vue
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<input type="hidden" :name="_prefix + 'TOTAL_FORMS'" :value="items.length || 0"/>
|
||||||
|
<template v-for="(value,name) in formData.management" v-bind:key="name">
|
||||||
|
<input type="hidden" :name="_prefix + name.toUpperCase()"
|
||||||
|
:value="value"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-rows ref="rows" :set="set" :context="this"
|
||||||
|
:columns="visibleFields" :columnsOrderable="columnsOrderable"
|
||||||
|
:orderable="orderable" @move="moveItem" @colmove="onColumnMove"
|
||||||
|
@cell="e => $emit('cell', e)">
|
||||||
|
|
||||||
|
<template #header-head>
|
||||||
|
<template v-if="orderable">
|
||||||
|
<th style="max-width:2em" :title="orderField.label"
|
||||||
|
:aria-label="orderField.label"
|
||||||
|
:aria-description="orderField.help || ''">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-arrow-down-1-9"></i>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<slot name="rows-header-head"></slot>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #row-head="data">
|
||||||
|
<input v-if="orderable" type="hidden"
|
||||||
|
:name="_prefix + data.row + '-' + orderBy"
|
||||||
|
:value="data.row"/>
|
||||||
|
<input type="hidden" :name="_prefix + data.row + '-id'"
|
||||||
|
:value="data.item ? data.item.id : ''"/>
|
||||||
|
|
||||||
|
<template v-for="field of hiddenFields" v-bind:key="field.name">
|
||||||
|
<input type="hidden"
|
||||||
|
v-if="!(field.name in ['id', orderBy])"
|
||||||
|
:name="_prefix + data.row + '-' + field.name"
|
||||||
|
:value="field.value in [null, undefined] ? data.item.data[name] : field.value"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<slot name="row-head" v-bind="data">
|
||||||
|
<td v-if="orderable">{{ data.row+1 }}</td>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-for="(field,slot) of fieldSlots" v-bind:key="field.name"
|
||||||
|
v-slot:[slot]="data">
|
||||||
|
<slot :name="slot" v-bind="data" :field="field" :input-name="_prefix + data.cell.row + '-' + field.name">
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<slot :name="'control-' + field.name" v-bind="data" :field="field" :input-name="_prefix + data.cell.row + '-' + field.name"/>
|
||||||
|
</div>
|
||||||
|
<p v-for="[error,index] in data.item.error(field.name)" class="help is-danger" v-bind:key="index">
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #row-tail="data">
|
||||||
|
<slot v-if="$slots['row-tail']" name="row-tail" v-bind="data"/>
|
||||||
|
<td class="align-right pr-0">
|
||||||
|
<button type="button" class="button square"
|
||||||
|
@click.stop="removeItem(data.row, data.item)"
|
||||||
|
:title="labels.remove_item"
|
||||||
|
:aria-label="labels.remove_item">
|
||||||
|
<span class="icon"><i class="fa fa-trash" /></span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</a-rows>
|
||||||
|
<div class="a-formset-footer flex-row">
|
||||||
|
<div class="flex-grow-1 flex-row">
|
||||||
|
<slot name="footer"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 align-right">
|
||||||
|
<button type="button" class="button square is-warning p-2"
|
||||||
|
@click="reset()"
|
||||||
|
:title="labels.discard_changes"
|
||||||
|
:aria-label="labels.discard_changes"
|
||||||
|
>
|
||||||
|
<span class="icon"><i class="fa fa-rotate" /></span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button square is-primary p-2"
|
||||||
|
@click="onActionAdd"
|
||||||
|
:title="labels.add_item"
|
||||||
|
:aria-label="labels.add_item"
|
||||||
|
>
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-plus"/></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import {cloneDeep} from 'lodash'
|
||||||
|
import Model, {Set} from '../model'
|
||||||
|
|
||||||
|
import ARows from './ARows'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emit: ['cell', 'move', 'colmove', 'load'],
|
||||||
|
components: {ARows},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
labels: Object,
|
||||||
|
|
||||||
|
//! If provided call this function instead of adding an item to rows on "+" button click.
|
||||||
|
actionAdd: Function,
|
||||||
|
|
||||||
|
//! If True, columns can be reordered
|
||||||
|
columnsOrderable: Boolean,
|
||||||
|
//! Field name used for ordering
|
||||||
|
orderBy: String,
|
||||||
|
|
||||||
|
//! Formset data as returned by get_formset_data
|
||||||
|
formData: Object,
|
||||||
|
//! Model class used for item's set
|
||||||
|
model: {type: Function, default: Model},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
set: new Set(Model),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
// ---- fields
|
||||||
|
_prefix() { return this.formData.prefix ? this.formData.prefix + '-' : '' },
|
||||||
|
fields() { return this.formData.fields },
|
||||||
|
orderField() { return this.orderBy && this.fields.find(f => f.name == this.orderBy) },
|
||||||
|
orderable() { return !!this.orderField },
|
||||||
|
|
||||||
|
hiddenFields() { return this.fields.filter(f => f.hidden && !(this.orderable && f == this.orderField)) },
|
||||||
|
visibleFields() { return this.fields.filter(f => !f.hidden) },
|
||||||
|
|
||||||
|
fieldSlots() { return this.visibleFields.reduce(
|
||||||
|
(slots, f) => ({...slots, ['row-' + f.name]: f}),
|
||||||
|
{}
|
||||||
|
)},
|
||||||
|
|
||||||
|
items() { return this.set.items },
|
||||||
|
rows() { return this.$refs.rows },
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onCellEvent(event) { this.$emit('cell', event) },
|
||||||
|
onColumnMove(event) { this.$emit('colmove', event) },
|
||||||
|
onActionAdd() {
|
||||||
|
if(this.actionAdd)
|
||||||
|
return this.actionAdd(this)
|
||||||
|
this.set.push()
|
||||||
|
},
|
||||||
|
|
||||||
|
moveItem(event) {
|
||||||
|
const {from, to} = event
|
||||||
|
const set_ = event.set || this.set
|
||||||
|
set_.move(from, to);
|
||||||
|
this.$emit('move', {...event, seŧ: set_})
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem(row) {
|
||||||
|
const item = this.items[row]
|
||||||
|
if(item.id) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.items.splice(row,1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//! Load items into set
|
||||||
|
load(items=[], reset=false) {
|
||||||
|
if(reset)
|
||||||
|
this.set.items = []
|
||||||
|
for(var item of items)
|
||||||
|
this.set.push(cloneDeep(item))
|
||||||
|
this.$emit('load', items)
|
||||||
|
},
|
||||||
|
|
||||||
|
//! Reset forms to initials
|
||||||
|
reset() {
|
||||||
|
this.load(this.formData?.initials || [], true)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
105
radiocampus/assets/src/components/AList.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- FIXME: header and footer should be inside list tags -->
|
||||||
|
<slot name="header"></slot>
|
||||||
|
<component :is="listTag" :class="listClass">
|
||||||
|
<template v-for="(item,index) in items" :key="index">
|
||||||
|
<component :is="itemTag" :class="itemClass" @click="select(index)"
|
||||||
|
:draggable="orderable" :data-index="index"
|
||||||
|
@dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop">
|
||||||
|
<slot name="item" :selected="index == selectedIndex" :set="set" :index="index" :item="item"></slot>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
</component>
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
emits: ['select', 'unselect', 'move', 'remove'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedIndex: this.defaultIndex,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
listClass: String,
|
||||||
|
itemClass: String,
|
||||||
|
defaultIndex: { type: Number, default: -1},
|
||||||
|
set: Object,
|
||||||
|
orderable: { type: Boolean, default: false },
|
||||||
|
itemTag: { default: 'li' },
|
||||||
|
listTag: { default: 'ul' },
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
model() { return this.set.model },
|
||||||
|
items() { return this.set.items },
|
||||||
|
length() { return this.set.length },
|
||||||
|
|
||||||
|
selected() {
|
||||||
|
return this.selectedIndex > -1 && this.items.length > this.selectedIndex > -1
|
||||||
|
? this.items[this.selectedIndex] : null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
get(index) { return this.set.get(index) },
|
||||||
|
find(pred) { return this.set.find(pred) },
|
||||||
|
findIndex(pred) { return this.set.findIndex(pred) },
|
||||||
|
|
||||||
|
remove(index, select=false) {
|
||||||
|
const item = this.set.get(index)
|
||||||
|
if(!item)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.set.remove(index);
|
||||||
|
if(index < this.selectedIndex)
|
||||||
|
this.selectedIndex--;
|
||||||
|
if(select && this.selectedIndex == index)
|
||||||
|
this.select(index)
|
||||||
|
this.$emit('remove', {index, item, set: this.set})
|
||||||
|
},
|
||||||
|
|
||||||
|
select(index) {
|
||||||
|
this.selectedIndex = index > -1 && this.items.length ? index % this.items.length : -1;
|
||||||
|
this.$emit('select', { item: this.selected, index: this.selectedIndex });
|
||||||
|
return this.selectedIndex;
|
||||||
|
},
|
||||||
|
|
||||||
|
unselect() {
|
||||||
|
this.$emit('unselect', { item: this.selected, index: this.selectedIndex});
|
||||||
|
this.selectedIndex = -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragStart(ev) {
|
||||||
|
const dataset = ev.target.dataset;
|
||||||
|
const data = `row:${dataset.index}`
|
||||||
|
ev.dataTransfer.setData("text/cell", data)
|
||||||
|
ev.dataTransfer.dropEffect = 'move'
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragOver(ev) {
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.dataTransfer.dropEffect = 'move'
|
||||||
|
},
|
||||||
|
|
||||||
|
onDrop(ev) {
|
||||||
|
const data = ev.dataTransfer.getData("text/cell")
|
||||||
|
if(!data || !data.startsWith('row:'))
|
||||||
|
return
|
||||||
|
|
||||||
|
ev.preventDefault()
|
||||||
|
const from = Number(data.slice(4))
|
||||||
|
const target = ev.target.tagName == this.itemTag ? ev.target
|
||||||
|
: ev.target.closest(this.itemTag)
|
||||||
|
this.$emit('move', {
|
||||||
|
from, target,
|
||||||
|
to: Number(target.dataset.index),
|
||||||
|
set: this.set,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
109
radiocampus/assets/src/components/AManyToManyEdit.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<div class="a-m2m-edit">
|
||||||
|
<table class="table is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<slot name="items-title"></slot>
|
||||||
|
</th>
|
||||||
|
<th style="width: 1rem">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-trash"/>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-for="item of items" :key="item.id">
|
||||||
|
<tr :class="[item.created && 'has-text-info', item.deleted && 'has-text-danger']">
|
||||||
|
<td>
|
||||||
|
<slot name="item" :item="item">
|
||||||
|
{{ item.data }}
|
||||||
|
</slot>
|
||||||
|
</td>
|
||||||
|
<td class="align-center">
|
||||||
|
<input type="checkbox" class="checkbox" @change="item.deleted = $event.target.checked">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-plus"/>
|
||||||
|
</span>
|
||||||
|
Add
|
||||||
|
</label>
|
||||||
|
<a-autocomplete ref="autocomplete" v-bind="autocomplete"
|
||||||
|
@select="onSelect">
|
||||||
|
<template #item="{item}">
|
||||||
|
<slot name="autocomplete-item" :item="item">{{ item }}</slot>
|
||||||
|
</template>
|
||||||
|
</a-autocomplete>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Model, { Set } from "../model.js"
|
||||||
|
import AAutocomplete from "./AAutocomplete.vue"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {AAutocomplete},
|
||||||
|
props: {
|
||||||
|
model: {type: Function, default: Model },
|
||||||
|
// List url
|
||||||
|
url: String,
|
||||||
|
// POST url
|
||||||
|
commitUrl: String,
|
||||||
|
// v-bind to autocomplete search box
|
||||||
|
autocomplete: {type: Object },
|
||||||
|
|
||||||
|
source_id: Number,
|
||||||
|
source_field: String,
|
||||||
|
target_field: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
set: new Set(this.model, {url: this.url, unique: true}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
items() { return this.set?.items || [] },
|
||||||
|
initials() {
|
||||||
|
let obj = {}
|
||||||
|
obj[this.source_id_attr] = this.source_id
|
||||||
|
return obj
|
||||||
|
},
|
||||||
|
|
||||||
|
source_id_attr() { return this.source_field + "_id" },
|
||||||
|
target_id_attr() { return this.target_field + "_id" },
|
||||||
|
target_ids() { return this.set?.items.map(i => i.data[this.target_id_attr]) },
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onSelect(index, item, value) {
|
||||||
|
if(this.target_ids.indexOf(item.id) != -1)
|
||||||
|
return
|
||||||
|
|
||||||
|
let obj = {...this.initials}
|
||||||
|
obj[this.target_field] = {...item}
|
||||||
|
obj[this.target_id_attr] = item.id
|
||||||
|
this.set.push(obj)
|
||||||
|
this.$refs.autocomplete.reset()
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.set.commit(this.commitUrl, {
|
||||||
|
fields: [...Object.keys(this.initials), this.target_id_attr]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.set.fetch()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
53
radiocampus/assets/src/components/AModal.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<section :class="['modal', active && 'is-active' || '']">
|
||||||
|
<div class="modal-background" @click="close"></div>
|
||||||
|
<div class="modal-card">
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<div class="modal-card-title">
|
||||||
|
<slot name="title" :item="item">{{ title }}</slot>
|
||||||
|
</div>
|
||||||
|
<slot name="bar" :item="item"></slot>
|
||||||
|
<button type="button" class="delete square" aria-label="close" @click="close">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-close"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body">
|
||||||
|
<slot name="default" :item="item"></slot>
|
||||||
|
</section>
|
||||||
|
<div class="modal-card-foot align-right">
|
||||||
|
<slot name="footer" :item="item" :close="close"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
title: { type: String, default: ""},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
///! If true, modal is open
|
||||||
|
active: false,
|
||||||
|
///! Item or data passed down to slots.
|
||||||
|
item: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
///! Open modal dialog. Set provided `item` to dialog's one.
|
||||||
|
open(item=null) {
|
||||||
|
this.active = true
|
||||||
|
this.item = item
|
||||||
|
},
|
||||||
|
///! Close modal and reset item to null.
|
||||||
|
close() {
|
||||||
|
this.active = false
|
||||||
|
this.item = null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
18
radiocampus/assets/src/components/APage.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
page: Object,
|
||||||
|
title: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
283
radiocampus/assets/src/components/APlayer.vue
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
<template>
|
||||||
|
<div class="a-player">
|
||||||
|
<div :class="['a-player-panels', panel ? 'is-open' : '']">
|
||||||
|
<template v-for="(info, key) in playlists" v-bind:key="key">
|
||||||
|
<APlaylist
|
||||||
|
:ref="key" class="a-player-panel a-playlist"
|
||||||
|
v-show="panel == key && sets[key].length"
|
||||||
|
:actions="['page', key != 'pin' && 'pin' || '']"
|
||||||
|
:editable="true" :player="self" :set="sets[key]"
|
||||||
|
@select="togglePlay(key, $event.index)"
|
||||||
|
listClass="menu-list" itemClass="menu-item">
|
||||||
|
<template v-slot:header="">
|
||||||
|
<div class="title is-flex-grow-1">
|
||||||
|
<span class="icon">
|
||||||
|
<i :class="info[1]"></i>
|
||||||
|
</span>
|
||||||
|
{{ info[0] }}
|
||||||
|
</div>
|
||||||
|
<button class="action button no-border">
|
||||||
|
<span class="icon" @click.stop="togglePanel()">
|
||||||
|
<i class="fa fa-close"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</APlaylist>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="a-player-progress" v-if="loaded && duration">
|
||||||
|
<AProgress v-if="loaded && duration" :value="currentTime" :max="this.duration"
|
||||||
|
:format="displayTime"
|
||||||
|
@select="audio.currentTime = $event"></AProgress>
|
||||||
|
</div>
|
||||||
|
<div class="a-player-bar button-group">
|
||||||
|
<button class="button" @click="togglePlay()"
|
||||||
|
:title="buttonTitle" :aria-label="buttonTitle">
|
||||||
|
<span class="fas fa-pause" v-if="playing"></span>
|
||||||
|
<span class="fas fa-play" v-else></span>
|
||||||
|
</button>
|
||||||
|
<div :class="['a-player-bar-content', loaded && duration ? 'has-progress' : '']">
|
||||||
|
<slot name="content" :loaded="loaded" :live="live" :current="current"></slot>
|
||||||
|
</div>
|
||||||
|
<button class="button has-text-weight-bold" v-if="loaded" @click="play()"
|
||||||
|
title="Live">
|
||||||
|
<span class="icon is-size-6 has-text-danger">
|
||||||
|
<span class="fa fa-circle"></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<template v-if="sets">
|
||||||
|
<template v-for="(info, key) in playlists" v-bind:key="key">
|
||||||
|
<button :class="playlistButtonClass(key)"
|
||||||
|
@click="togglePanel(key)"
|
||||||
|
v-show="sets[key] && sets[key].length">
|
||||||
|
<span class="is-size-6">{{ sets[key] && sets[key].length }}</span>
|
||||||
|
<span class="icon">
|
||||||
|
<i :class="info[1]"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {reactive} from 'vue'
|
||||||
|
import Live from '../live'
|
||||||
|
import Sound from '../sound'
|
||||||
|
import {Set} from '../model'
|
||||||
|
import APlaylist from './APlaylist'
|
||||||
|
import AProgress from './AProgress'
|
||||||
|
|
||||||
|
|
||||||
|
export const State = {
|
||||||
|
paused: 0,
|
||||||
|
playing: 1,
|
||||||
|
loading: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { APlaylist, AProgress },
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let audio = new Audio();
|
||||||
|
audio.addEventListener('ended', e => this.onState(e));
|
||||||
|
audio.addEventListener('pause', e => this.onState(e));
|
||||||
|
audio.addEventListener('playing', e => this.onState(e));
|
||||||
|
audio.addEventListener('timeupdate', () => {
|
||||||
|
this.currentTime = this.audio.currentTime;
|
||||||
|
});
|
||||||
|
audio.addEventListener('durationchange', () => {
|
||||||
|
this.duration = Number.isFinite(this.audio.duration) ? this.audio.duration : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
let live = this.liveArgs ? reactive(new Live(this.liveArgs)) : null;
|
||||||
|
live && live.refresh();
|
||||||
|
|
||||||
|
const sets = {}
|
||||||
|
for(const key in this.playlists)
|
||||||
|
sets[key] = Set.storeLoad(Sound, 'playlist.' + key,
|
||||||
|
{max: 30, unique: true})
|
||||||
|
|
||||||
|
return {
|
||||||
|
audio, duration: 0, currentTime: 0, state: State.paused,
|
||||||
|
live,
|
||||||
|
|
||||||
|
/// Loaded item
|
||||||
|
loaded: null,
|
||||||
|
//! Active panel name
|
||||||
|
panel: null,
|
||||||
|
//! current playing playlist name
|
||||||
|
playlistName: null,
|
||||||
|
//! players' playlists' sets
|
||||||
|
sets,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
buttonTitle: String,
|
||||||
|
liveArgs: Object,
|
||||||
|
///! dict of {'slug': ['Label', 'icon']}
|
||||||
|
playlists: Object,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
self() { return this; },
|
||||||
|
paused() { return this.state == State.paused; },
|
||||||
|
playing() { return this.state == State.playing; },
|
||||||
|
loading() { return this.state == State.loading; },
|
||||||
|
|
||||||
|
playlist() {
|
||||||
|
return this.playlistName ? this.$refs[this.playlistName][0] : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
current() {
|
||||||
|
return this.loaded ? this.loaded : this.live && this.live.current;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
displayTime(seconds) {
|
||||||
|
seconds = parseInt(seconds);
|
||||||
|
let s = seconds % 60;
|
||||||
|
seconds = (seconds - s) / 60;
|
||||||
|
let m = seconds % 60;
|
||||||
|
let h = (seconds - m) / 60;
|
||||||
|
|
||||||
|
let [ss,mm,hh] = [s.toString().padStart(2, '0'),
|
||||||
|
m.toString().padStart(2, '0'),
|
||||||
|
h.toString().padStart(2, '0')];
|
||||||
|
return h ? `${hh}:${mm}:${ss}` : `${mm}:${ss}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
playlistButtonClass(name) {
|
||||||
|
let set = this.sets[name];
|
||||||
|
return (set ? (set.length ? "" : "has-text-grey-light ")
|
||||||
|
+ (this.panel == name ? "open"
|
||||||
|
: this.playlistName == name ? 'active' : '') : '')
|
||||||
|
+ " button";
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Show/hide panel
|
||||||
|
togglePanel(panel) { this.panel = this.panel == panel ? null : panel },
|
||||||
|
/// Return True if item is loaded
|
||||||
|
isLoaded(item) { return this.loaded && this.loaded.id == item.id },
|
||||||
|
/// Return True if item is loaded
|
||||||
|
isPlaying(item) { return this.isLoaded(item) && !this.paused },
|
||||||
|
|
||||||
|
_setPlaylist(playlist) {
|
||||||
|
this.playlistName = playlist;
|
||||||
|
for(var p in this.sets)
|
||||||
|
if(p != playlist && this.$refs[p])
|
||||||
|
this.$refs[p][0].unselect();
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Load a sound from playlist or live
|
||||||
|
load(playlist=null, index=0) {
|
||||||
|
let src = null;
|
||||||
|
|
||||||
|
// from playlist
|
||||||
|
if(playlist !== null && index != -1) {
|
||||||
|
let item = this.$refs[playlist][0].get(index);
|
||||||
|
if(!item)
|
||||||
|
throw `No sound at index ${index} for playlist ${playlist}`;
|
||||||
|
this.loaded = item
|
||||||
|
src = item.src;
|
||||||
|
}
|
||||||
|
// from live
|
||||||
|
else {
|
||||||
|
this.loaded = null;
|
||||||
|
src = this.live.src;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._setPlaylist(playlist);
|
||||||
|
|
||||||
|
// load sources
|
||||||
|
const audio = this.audio;
|
||||||
|
if(src instanceof Array) {
|
||||||
|
audio.innerHTML = '';
|
||||||
|
audio.removeAttribute('src');
|
||||||
|
for(var s of src) {
|
||||||
|
let source = document.createElement('source');
|
||||||
|
source.setAttribute('src', s);
|
||||||
|
audio.appendChild(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
audio.src = src;
|
||||||
|
}
|
||||||
|
audio.load();
|
||||||
|
},
|
||||||
|
|
||||||
|
play(playlist=null, index=0) {
|
||||||
|
this.load(playlist, index);
|
||||||
|
this.audio.play().catch(e => console.error(e))
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Push items to playlist (by name)
|
||||||
|
push(playlist, ...items) {
|
||||||
|
return this.sets[playlist].push(...items);
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Push and play items
|
||||||
|
playItems(playlist, ...items) {
|
||||||
|
let index = this.push(playlist, ...items);
|
||||||
|
this.$refs[playlist][0].selectedIndex = index;
|
||||||
|
this.play(playlist, index);
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Handle click event that plays multiple items (from `data-sounds` attribute)
|
||||||
|
playButtonClick(event) {
|
||||||
|
var items = JSON.parse(event.currentTarget.dataset.sounds);
|
||||||
|
this.playItems('queue', ...items);
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Pause
|
||||||
|
pause() {
|
||||||
|
this.audio.pause()
|
||||||
|
},
|
||||||
|
|
||||||
|
//! Play/pause
|
||||||
|
togglePlay(playlist=null, index=0) {
|
||||||
|
if(playlist !== null) {
|
||||||
|
this.panel = null;
|
||||||
|
let item = this.sets[playlist].get(index);
|
||||||
|
if(!this.playlist || this.playlistName !== playlist || this.loaded != item) {
|
||||||
|
this.play(playlist, index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(this.paused)
|
||||||
|
this.audio.play().catch(e => console.error(e))
|
||||||
|
else
|
||||||
|
this.audio.pause();
|
||||||
|
},
|
||||||
|
|
||||||
|
//! Pin/Unpin an item
|
||||||
|
togglePlaylist(playlist, item) {
|
||||||
|
const set = this.sets[playlist]
|
||||||
|
let index = set.findIndex(item);
|
||||||
|
if(index > -1)
|
||||||
|
set.remove(index);
|
||||||
|
else {
|
||||||
|
set.push(item);
|
||||||
|
// this.$refs.pinPlaylistButton.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Audio player state change event
|
||||||
|
onState(event) {
|
||||||
|
const audio = this.audio;
|
||||||
|
this.state = audio.paused ? State.paused : State.playing;
|
||||||
|
|
||||||
|
if(event.type == 'ended' && (!this.playlist || this.playlist.selectNext() == -1))
|
||||||
|
this.play();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
65
radiocampus/assets/src/components/APlaylist.vue
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="a-playlist">
|
||||||
|
<div class="header"><slot name="header"></slot></div>
|
||||||
|
<ul :class="listClass">
|
||||||
|
<li v-for="(item,index) in items" :class="[itemClass, player.isPlaying(item) ? 'is-active' : '']" @click="!hasAction('play') && select(index)"
|
||||||
|
:key="index">
|
||||||
|
<ASoundItem
|
||||||
|
:data="item" :index="index" :set="set" :player="player_"
|
||||||
|
@togglePlay="togglePlay(index)"
|
||||||
|
:actions="actions">
|
||||||
|
<template #after-title="bindings">
|
||||||
|
<slot name="after-title" v-bind="bindings"></slot>
|
||||||
|
</template>
|
||||||
|
<template #actions="bindings">
|
||||||
|
<slot name="actions" v-bind="bindings"></slot>
|
||||||
|
<button class="button" v-if="editable" @click.stop="remove(index,true)">
|
||||||
|
<span class="icon is-small"><span class="fa fa-close"></span></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</ASoundItem>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import AList from './AList';
|
||||||
|
import ASoundItem from './ASoundItem';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: AList,
|
||||||
|
emits: [...AList.emits],
|
||||||
|
components: { ASoundItem },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
actions: Array,
|
||||||
|
// FIXME: remove
|
||||||
|
name: String,
|
||||||
|
player: Object,
|
||||||
|
editable: Boolean,
|
||||||
|
withLink: Boolean
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
self() { return this; },
|
||||||
|
player_() { return this.player || window.aircox.player },
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
hasAction(action) { return this.actions && this.actions.indexOf(action) != -1; },
|
||||||
|
|
||||||
|
selectNext() {
|
||||||
|
let index = this.selectedIndex + 1;
|
||||||
|
return this.select(index >= this.items.length ? -1 : index);
|
||||||
|
},
|
||||||
|
|
||||||
|
togglePlay(index) {
|
||||||
|
if(this.player_.isPlaying(this.set.get(index)))
|
||||||
|
this.player_.pause();
|
||||||
|
else
|
||||||
|
this.select(index)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
71
radiocampus/assets/src/components/AProgress.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div class="a-progress m-0">
|
||||||
|
<time class="time-now">
|
||||||
|
<slot name="value" :value="value" :max="max">{{ format(value) }}</slot>
|
||||||
|
</time>
|
||||||
|
<div ref="bar" class="a-progress-bar-container" @click.stop="onClick" @mouseleave.stop="onMouseMove"
|
||||||
|
@mousemove.stop="onMouseMove">
|
||||||
|
<div :class="progressClass" :style="progressStyle">
|
||||||
|
<time v-if="hoverValue">
|
||||||
|
{{ format(hoverValue) }}
|
||||||
|
</time>
|
||||||
|
<template v-else> </template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<time class="time-total">
|
||||||
|
<slot name="value" :value="valueDisplay" :max="max">{{ format(max) }}</slot>
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hoverValue: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: Number,
|
||||||
|
max: Number,
|
||||||
|
format: { type: Function, default: x => x },
|
||||||
|
progressClass: { default: 'a-progress-bar' },
|
||||||
|
vertical: { type: Boolean, default: false },
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
valueDisplay() { return this.hoverValue === null ? this.value : this.hoverValue; },
|
||||||
|
|
||||||
|
progressStyle() {
|
||||||
|
if(!this.max)
|
||||||
|
return null;
|
||||||
|
let value = this.max ? this.valueDisplay * 100 / this.max : 0;
|
||||||
|
return this.vertical ? { height: `${value}%` } : { width: `${value}%` };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
xToValue(x) { return x * this.max / this.$refs.bar.getBoundingClientRect().width },
|
||||||
|
yToValue(y) { return y * this.max / this.$refs.bar.getBoundingClientRect().height },
|
||||||
|
|
||||||
|
valueFromEvent(event) {
|
||||||
|
let rect = event.currentTarget.getBoundingClientRect()
|
||||||
|
return this.vertical ? this.yToValue(event.clientY - rect.y)
|
||||||
|
: this.xToValue(event.clientX - rect.x);
|
||||||
|
},
|
||||||
|
|
||||||
|
onClick(event) {
|
||||||
|
this.$emit('select', this.valueFromEvent(event));
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseMove(event) {
|
||||||
|
if(event.type == 'mouseleave')
|
||||||
|
this.hoverValue = null;
|
||||||
|
else {
|
||||||
|
this.hoverValue = this.valueFromEvent(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
151
radiocampus/assets/src/components/ARow.vue
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<template>
|
||||||
|
<tr>
|
||||||
|
<slot name="head" :context="context" :item="item" :row="row"/>
|
||||||
|
<template v-for="(attr,col) in columns" :key="col">
|
||||||
|
<slot name="cell-before" :context="context" :item="item" :cell="cells[col]"
|
||||||
|
:attr="attr"/>
|
||||||
|
<component :is="cellTag" :class="['cell', 'cell-' + attr]" :data-col="col"
|
||||||
|
:draggable="orderable"
|
||||||
|
@dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop">
|
||||||
|
<slot :name="attr" :context="context" :item="item" :cell="cells[col]"
|
||||||
|
:data="itemData" :attr="attr" :emit="cellEmit"
|
||||||
|
:value="itemData && itemData[attr]">
|
||||||
|
{{ itemData && itemData[attr] }}
|
||||||
|
</slot>
|
||||||
|
<slot name="cell" :context="context" :item="item" :cell="cells[col]"
|
||||||
|
:data="itemData" :attr="attr" :emit="cellEmit"
|
||||||
|
:value="itemData && itemData[attr]"/>
|
||||||
|
</component>
|
||||||
|
<slot name="cell-after" :context="context" :item="item" :col="col" :cell="cells[col]"
|
||||||
|
:attr="attr"/>
|
||||||
|
</template>
|
||||||
|
<slot name="tail" :context="context" :item="item" :row="row"/>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import {isReactive, toRefs} from 'vue'
|
||||||
|
import Model from '../model'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ['move', 'cell'],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
//! Context object
|
||||||
|
context: {type: Object, default: () => ({})},
|
||||||
|
//! Item to display in row
|
||||||
|
item: {type: Object, default: () => ({})},
|
||||||
|
//! Columns to display, as items' attributes
|
||||||
|
//! - name: field name / item attribute value
|
||||||
|
//! - label: display label
|
||||||
|
//! - help: help text
|
||||||
|
columns: Array,
|
||||||
|
//! Default cell's info
|
||||||
|
cell: {type: Object, default() { return {row: 0}}},
|
||||||
|
//! Cell component tag
|
||||||
|
cellTag: {type: String, default: 'td'},
|
||||||
|
//! If true, can reorder cell by drag & drop
|
||||||
|
orderable: {type: Boolean, default: false},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Row index
|
||||||
|
*/
|
||||||
|
row() { return this.cell && this.cell.row || 0 },
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item's data if model instance, otherwise item
|
||||||
|
*/
|
||||||
|
itemData() {
|
||||||
|
return this.item instanceof Model ? this.item.data : this.item;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computed cell infos
|
||||||
|
*/
|
||||||
|
cells() {
|
||||||
|
const cell = isReactive(this.cell) && toRefs(this.cell) || this.cell || {}
|
||||||
|
const cells = []
|
||||||
|
for(var col in this.columns)
|
||||||
|
cells.push({...cell, col: Number(col)})
|
||||||
|
return cells
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Emit a 'cell' event.
|
||||||
|
* Event data: `{name, cell, data, item}`
|
||||||
|
* @param {Number} col: cell column's index
|
||||||
|
* @param {String} name: cell's event name
|
||||||
|
* @param {} data: cell's event data
|
||||||
|
*/
|
||||||
|
cellEmit(name, cell, data) {
|
||||||
|
this.$emit('cell', {
|
||||||
|
name, cell, data,
|
||||||
|
item: this.item,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragStart(ev) {
|
||||||
|
const dataset = ev.target.dataset;
|
||||||
|
const data = `cell:${dataset.col}`
|
||||||
|
ev.dataTransfer.setData("text/cell", data)
|
||||||
|
ev.dataTransfer.dropEffect = 'move'
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragOver(ev) {
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.dataTransfer.dropEffect = 'move'
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle drop event, emit `'move': { from, to }`.
|
||||||
|
*/
|
||||||
|
onDrop(ev) {
|
||||||
|
const data = ev.dataTransfer.getData("text/cell")
|
||||||
|
if(!data || !data.startsWith('cell:'))
|
||||||
|
return
|
||||||
|
|
||||||
|
ev.preventDefault()
|
||||||
|
this.$emit('move', {
|
||||||
|
from: Number(data.slice(5)),
|
||||||
|
to: Number(ev.target.dataset.col),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return DOM node for cells at provided position `col`
|
||||||
|
*/
|
||||||
|
getCellEl(col) {
|
||||||
|
const els = this.$el.querySelectorAll(this.cellTag)
|
||||||
|
for(var el of els)
|
||||||
|
if(col == Number(el.dataset.col))
|
||||||
|
return el;
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focus cell's form input. If from is provided, related focus
|
||||||
|
*/
|
||||||
|
focus(col, from) {
|
||||||
|
if(from)
|
||||||
|
col += from.col
|
||||||
|
|
||||||
|
const target = this.getCellEl(col)
|
||||||
|
if(!target)
|
||||||
|
return
|
||||||
|
const control = target.querySelector('input:not([type="hidden"])') ||
|
||||||
|
target.querySelector('button') ||
|
||||||
|
target.querySelector('select') ||
|
||||||
|
target.querySelector('a');
|
||||||
|
control && control.focus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$el.__row = this
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
153
radiocampus/assets/src/components/ARows.vue
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<table class="table is-stripped is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<a-row :context="context" :columns="columnNames"
|
||||||
|
:orderable="columnsOrderable" cellTag="th"
|
||||||
|
@move="moveColumn">
|
||||||
|
<template v-if="$slots['header-head']" v-slot:head="data">
|
||||||
|
<slot name="header-head" v-bind="data"/>
|
||||||
|
</template>
|
||||||
|
<template v-if="$slots['header-tail']" v-slot:tail="data">
|
||||||
|
<slot name="header-tail" v-bind="data"/>
|
||||||
|
</template>
|
||||||
|
<template v-for="column of columns" v-bind:key="column.name"
|
||||||
|
v-slot:[column.name]="data">
|
||||||
|
<slot :name="'header-' + column.name" v-bind="data">
|
||||||
|
{{ column.label }}
|
||||||
|
<span v-if="column.help" class="icon small"
|
||||||
|
:title="column.help">
|
||||||
|
<i class="fa fa-circle-question"/>
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</a-row>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<slot name="head"/>
|
||||||
|
<template v-for="(item,row) in items" :key="row">
|
||||||
|
<!-- data-index comes from AList component drag & drop -->
|
||||||
|
<a-row :context="context" :item="item" :cell="{row}" :columns="columnNames" :data-index="row"
|
||||||
|
:data-row="row"
|
||||||
|
:draggable="orderable"
|
||||||
|
@dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop"
|
||||||
|
@cell="onCellEvent(row, $event)">
|
||||||
|
<template v-for="[name,slot] of rowSlots" :key="slot" v-slot:[slot]="data">
|
||||||
|
<slot :name="name" v-bind="data"/>
|
||||||
|
</template>
|
||||||
|
</a-row>
|
||||||
|
</template>
|
||||||
|
<slot name="tail"/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import AList from './AList.vue'
|
||||||
|
import ARow from './ARow.vue'
|
||||||
|
|
||||||
|
const Component = {
|
||||||
|
extends: AList,
|
||||||
|
components: { ARow },
|
||||||
|
//! Event:
|
||||||
|
//! - cell(event): an event occured inside cell
|
||||||
|
//! - colmove({from,to}), colmove(): columns moved
|
||||||
|
emits: ['cell', 'colmove'],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
...AList.props,
|
||||||
|
//! Context object
|
||||||
|
context: {type: Object, default: () => ({})},
|
||||||
|
|
||||||
|
//! Ordered list of columns, as objects with:
|
||||||
|
//! - name: item attribute value
|
||||||
|
//! - label: display label
|
||||||
|
//! - help: help text
|
||||||
|
//! - hidden: if true, field is hidden
|
||||||
|
columns: Array,
|
||||||
|
//! If True, columns are orderable
|
||||||
|
columnsOrderable: Boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
...super.data,
|
||||||
|
// TODO: add observer
|
||||||
|
columns_: [...this.columns],
|
||||||
|
extraItem: new this.set.model(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
columnNames() { return this.columns_.map(c => c.name) },
|
||||||
|
columnLabels() { return this.columns_.reduce(
|
||||||
|
(labels, c) => ({...labels, [c.name]: c.label}),
|
||||||
|
{}
|
||||||
|
)},
|
||||||
|
rowSlots() {
|
||||||
|
return Object.keys(this.$slots).filter(x => x.startsWith('row-'))
|
||||||
|
.map(x => [x, x.slice(4)])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// TODO: use in tracklist
|
||||||
|
sortColumns(names) {
|
||||||
|
const ordered = names.map(n => this.columns_.find(c => c.name == n)).filter(c => !!c);
|
||||||
|
const remaining = this.columns_.filter(c => names.indexOf(c.name) == -1)
|
||||||
|
this.columns_ = [...ordered, ...remaining]
|
||||||
|
this.$emit('colmove')
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move column using provided event object (as `{from, to}`)
|
||||||
|
*/
|
||||||
|
moveColumn(event) {
|
||||||
|
const {from, to} = event
|
||||||
|
const value = this.columns_[from]
|
||||||
|
this.columns_.splice(from, 1)
|
||||||
|
this.columns_.splice(to, 0, value)
|
||||||
|
this.$emit('colmove', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React on 'cell' event, re-emitting it with additional values:
|
||||||
|
* - `set`: data set
|
||||||
|
* - `row`: row index
|
||||||
|
*
|
||||||
|
* @param {Number} row: row index
|
||||||
|
* @param {} data: cell's event data
|
||||||
|
*/
|
||||||
|
onCellEvent(row, event) {
|
||||||
|
if(event.name == 'focus')
|
||||||
|
this.focus(event.data, event.cell)
|
||||||
|
this.$emit('cell', {
|
||||||
|
...event, row,
|
||||||
|
set: this.set
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return row component at provided index
|
||||||
|
*/
|
||||||
|
getRow(row) {
|
||||||
|
const els = this.$el.querySelectorAll('tr')
|
||||||
|
for(var el of els)
|
||||||
|
if(el.__row && row == Number(el.dataset.row))
|
||||||
|
return el.__row
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focus on a cell
|
||||||
|
*/
|
||||||
|
focus(row, col, from=null) {
|
||||||
|
if(from)
|
||||||
|
row += from.row
|
||||||
|
row = this.getRow(row)
|
||||||
|
row && row.focus(col, from)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Component.props.itemTag.default = 'tr'
|
||||||
|
Component.props.listTag.default = 'tbody'
|
||||||
|
|
||||||
|
export default Component
|
||||||
|
</script>
|
167
radiocampus/assets/src/components/ASelectFile.vue
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal ref="modal" :title="title">
|
||||||
|
<template #bar>
|
||||||
|
<button type="button" class="button small mr-3" v-if="panel == LIST"
|
||||||
|
@click="showPanel(UPLOAD)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-upload"></i>
|
||||||
|
</span>
|
||||||
|
<span>{{ labels.upload }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="button small mr-3" v-else
|
||||||
|
@click="showPanel(LIST)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-list"></i>
|
||||||
|
</span>
|
||||||
|
<span>{{ labels.list }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<a-file-upload ref="upload" v-if="panel == UPLOAD"
|
||||||
|
:url="uploadUrl"
|
||||||
|
:label="uploadLabel" :field-name="uploadFieldName"
|
||||||
|
@load="uploadDone">
|
||||||
|
<template #form="data">
|
||||||
|
<slot name="upload-form" v-bind="data"></slot>
|
||||||
|
</template>
|
||||||
|
<template #preview="data">
|
||||||
|
<slot name="upload-preview" v-bind="data"></slot>
|
||||||
|
</template>
|
||||||
|
</a-file-upload>
|
||||||
|
<div class="a-select-file" v-else>
|
||||||
|
<div ref="list"
|
||||||
|
:class="['a-select-file-list', listClass]">
|
||||||
|
<!-- tiles -->
|
||||||
|
<div v-if="prevUrl">
|
||||||
|
<a href="#" @click="load(prevUrl)">
|
||||||
|
{{ labels.show_previous }}
|
||||||
|
</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" :load="load" :lastUrl="lastUrl"></slot>
|
||||||
|
<a-action-button v-if="deleteUrl"
|
||||||
|
class="has-text-danger small float-right"
|
||||||
|
icon="fa fa-trash"
|
||||||
|
:confirm="labels.confirm_delete"
|
||||||
|
method="DELETE"
|
||||||
|
:url="deleteUrl.replace('123', item.id)"
|
||||||
|
@done="load(lastUrl)">
|
||||||
|
</a-action-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="nextUrl">
|
||||||
|
<a href="#" @click="load(nextUrl)">
|
||||||
|
{{ labels.show_next }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<slot name="footer" :item="item">
|
||||||
|
<span class="mr-3" v-if="item">{{ item.name }}</span>
|
||||||
|
</slot>
|
||||||
|
<button type="button" v-if="panel == LIST" class="button align-right"
|
||||||
|
@click="selected">
|
||||||
|
{{ labels.select_file }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import AModal from "./AModal"
|
||||||
|
import AActionButton from "./AActionButton"
|
||||||
|
import AFileUpload from "./AFileUpload"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emit: ["select"],
|
||||||
|
|
||||||
|
components: {AActionButton, AFileUpload, AModal},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
title: { type: String },
|
||||||
|
labels: Object,
|
||||||
|
listClass: {type: String, default: ""},
|
||||||
|
|
||||||
|
// List url
|
||||||
|
listUrl: { type: String },
|
||||||
|
|
||||||
|
// URL to delete an item, where "123" is replaced by
|
||||||
|
// the item id.
|
||||||
|
deleteUrl: {type: String },
|
||||||
|
|
||||||
|
uploadUrl: { type: String },
|
||||||
|
uploadFieldName: { type: String, default: "file" },
|
||||||
|
uploadLabel: { type: String, default: "Upload a file" },
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
LIST: 0,
|
||||||
|
UPLOAD: 1,
|
||||||
|
|
||||||
|
panel: 0,
|
||||||
|
item: null,
|
||||||
|
items: [],
|
||||||
|
nextUrl: "",
|
||||||
|
prevUrl: "",
|
||||||
|
lastUrl: "",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
open() {
|
||||||
|
this.$refs.modal.open()
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$refs.modal.close()
|
||||||
|
},
|
||||||
|
|
||||||
|
showPanel(panel) {
|
||||||
|
this.panel = panel
|
||||||
|
},
|
||||||
|
|
||||||
|
load(url) {
|
||||||
|
return fetch(url || this.listUrl).then(
|
||||||
|
response => response.ok ? response.json() : Promise.reject(response)
|
||||||
|
).then(data => {
|
||||||
|
this.lastUrl = url
|
||||||
|
this.nextUrl = data.next
|
||||||
|
this.prevUrl = data.previous
|
||||||
|
this.items = data.results
|
||||||
|
this.showPanel(this.LIST)
|
||||||
|
|
||||||
|
this.$forceUpdate()
|
||||||
|
this.$refs.list.scroll(0, 0)
|
||||||
|
return this.items
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
//! Select an item
|
||||||
|
select(item) {
|
||||||
|
this.item = item;
|
||||||
|
},
|
||||||
|
|
||||||
|
//! User click on select button (confirm selection)
|
||||||
|
selected() {
|
||||||
|
this.$emit("select", this.item)
|
||||||
|
this.close()
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadDone(reload=false) {
|
||||||
|
reload && this.load().then(items => {
|
||||||
|
this.item = items[0]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.load()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
63
radiocampus/assets/src/components/ASoundItem.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="['a-sound-item m-0 button-group', playing && 'playing' || '']">
|
||||||
|
<slot name="title" :player="player" :item="item" :loaded="loaded">
|
||||||
|
<span :class="['label is-flex-grow-1 align-left', playing && 'blink' || '']" @click.stop="$emit('togglePlay')">
|
||||||
|
{{ name || item.name }}
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
|
<slot name="after-title" :player="player" :item="item" :loaded="loaded">
|
||||||
|
</slot>
|
||||||
|
<div class="button-group actions">
|
||||||
|
<a class="button action" v-if="hasAction('page')"
|
||||||
|
:href="item.data.page_url">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<i class="fa fa-external-link"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a class="button action"
|
||||||
|
v-if="hasAction('download') && item.data.is_downloadable"
|
||||||
|
:href="item.data.url" target="_blank">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<span class="fa fa-download"></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<button :class="['button action', pinned ? 'selected' : 'not-selected']"
|
||||||
|
v-if="hasAction('pin') && player && player.sets.pin != $parent.set" @click.stop="player.togglePlaylist('pin', item)">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<span class="fa fa-star"></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<slot name="actions" :player="player" :item="item" :loaded="loaded"></slot>
|
||||||
|
</div>
|
||||||
|
<slot name="extra-right" :player="player" :item="item" :loaded="loaded"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Model from '../model';
|
||||||
|
import Sound from '../sound';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
data: {type: Object, default: () => {}},
|
||||||
|
name: String,
|
||||||
|
player: Object,
|
||||||
|
page_url: String,
|
||||||
|
actions: {type:Array, default: () => []},
|
||||||
|
index: {type:Number, default: null},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
item() { return this.data instanceof Model ? this.data : new Sound(this.data || {}); },
|
||||||
|
loaded() { return this.player && this.player.isLoaded(this.item) },
|
||||||
|
playing() { return this.player && this.player.isPlaying(this.item) },
|
||||||
|
paused() { return this.player && this.player.paused && this.loaded },
|
||||||
|
pinned() { return this.player && this.player.sets.pin.find(this.item) },
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
hasAction(action) {
|
||||||
|
return this.actions && this.actions.indexOf(action) != -1;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
83
radiocampus/assets/src/components/ASoundListEditor.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div class="a-playlist-editor">
|
||||||
|
<a-select-file ref="select-file"
|
||||||
|
:title="labels && labels.add_sound"
|
||||||
|
:labels="labels"
|
||||||
|
:list-url="soundListUrl"
|
||||||
|
:deleteUrl="soundDeleteUrl"
|
||||||
|
:uploadUrl="soundUploadUrl"
|
||||||
|
:uploadLabel="labels.select_file"
|
||||||
|
@select="selected"
|
||||||
|
>
|
||||||
|
<template #upload-preview="{upload}">
|
||||||
|
<slot name="upload-preview" :upload="upload"></slot>
|
||||||
|
</template>
|
||||||
|
<template #upload-form>
|
||||||
|
<slot name="upload-form"></slot>
|
||||||
|
</template>
|
||||||
|
<template #default="{item}">
|
||||||
|
<audio controls :src="item.url"></audio>
|
||||||
|
<label class="label small flex-grow-1">{{ item.name }}</label>
|
||||||
|
</template>
|
||||||
|
</a-select-file>
|
||||||
|
|
||||||
|
<a-form-set ref="formset" :form-data="formData" :labels="labels"
|
||||||
|
:initials="initData.items"
|
||||||
|
order-by="position"
|
||||||
|
:action-add="actionAdd">
|
||||||
|
<template v-for="[name,slot] of rowsSlots" :key="slot"
|
||||||
|
v-slot:[slot]="data">
|
||||||
|
<slot v-if="name != 'row-tail'" :name="name" v-bind="data"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #row-sound="{item,inputName}">
|
||||||
|
<label>{{ item.data.name }}</label><br>
|
||||||
|
<audio controls :src="item.data.url"/>
|
||||||
|
<input type="hidden" :name="inputName" :value="item.data.sound"/>
|
||||||
|
</template>
|
||||||
|
</a-form-set>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import AFormSet from './AFormSet'
|
||||||
|
import ASelectFile from "./ASelectFile"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {AFormSet, ASelectFile},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
formData: Object,
|
||||||
|
labels: Object,
|
||||||
|
// initial datas
|
||||||
|
initData: Object,
|
||||||
|
|
||||||
|
soundListUrl: String,
|
||||||
|
soundUploadUrl: String,
|
||||||
|
soundDeleteUrl: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
rowsSlots() {
|
||||||
|
return Object.keys(this.$slots)
|
||||||
|
.filter(x => x.startsWith('row-') || x.startsWith('rows-') || x.startsWith('control-'))
|
||||||
|
.map(x => [x, x.startsWith('rows-') ? x.slice(5) : x])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
actionAdd() {
|
||||||
|
this.$refs['select-file'].open()
|
||||||
|
},
|
||||||
|
|
||||||
|
selected(item) {
|
||||||
|
const data = {
|
||||||
|
"sound": item.id,
|
||||||
|
"name": item.name,
|
||||||
|
"url": item.url,
|
||||||
|
"broadcast": item.broadcast,
|
||||||
|
}
|
||||||
|
this.$refs.formset.set.push(data)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
41
radiocampus/assets/src/components/AStatistics.vue
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<form ref="form">
|
||||||
|
<slot :counts="counts"></slot>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const splitReg = new RegExp(',\\s*|\\s+', 'g');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
counts: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
const items = this.$el.querySelectorAll('input[name="data"]:checked')
|
||||||
|
const counts = {};
|
||||||
|
|
||||||
|
for(var item of items)
|
||||||
|
if(item.value)
|
||||||
|
for(var tag of item.value.split(splitReg))
|
||||||
|
if(tag.trim())
|
||||||
|
counts[tag.trim()] = (counts[tag.trim()] || 0) + 1;
|
||||||
|
this.counts = counts;
|
||||||
|
},
|
||||||
|
|
||||||
|
onclick() {
|
||||||
|
// TODO: row click => check checkbox
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
console.log(this.counts)
|
||||||
|
this.$refs.form.addEventListener('change', () => this.update())
|
||||||
|
this.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
57
radiocampus/assets/src/components/AStreamer.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<slot :streamer="streamer" :streamers="streamers" :Sound="Sound"
|
||||||
|
:sources="sources" :fetchStreamers="fetchStreamers"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Sound from '../sound';
|
||||||
|
import {setEcoInterval} from '../utils';
|
||||||
|
|
||||||
|
import Streamer from '../streamer';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
apiUrl: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// current streamer
|
||||||
|
streamer: null,
|
||||||
|
// all streamers
|
||||||
|
streamers: [],
|
||||||
|
// fetch interval id
|
||||||
|
fetchInterval: null,
|
||||||
|
Sound: Sound,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
sources() {
|
||||||
|
var sources = this.streamer ? this.streamer.sources : [];
|
||||||
|
return sources.filter(s => s.data)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
fetchStreamers() {
|
||||||
|
Streamer.fetch(this.apiUrl, {many:true}).then(streamers => {
|
||||||
|
this.streamers = streamers
|
||||||
|
this.streamer = streamers ? streamers[0] : null
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.fetchStreamers();
|
||||||
|
this.fetchInterval = setEcoInterval(() => this.streamer && this.streamer.fetch(), 5000)
|
||||||
|
},
|
||||||
|
|
||||||
|
unmounted() {
|
||||||
|
if(this.fetchInterval !== null)
|
||||||
|
clearInterval(this.fetchInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
80
radiocampus/assets/src/components/ASwitch.vue
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<button :title="ariaLabel"
|
||||||
|
type="button"
|
||||||
|
:aria-label="ariaLabel || label" :aria-description="ariaDescription"
|
||||||
|
@click="toggle" :class="buttonClass">
|
||||||
|
<slot name="default" :active="active">
|
||||||
|
<span class="icon">
|
||||||
|
<i :class="icon"></i>
|
||||||
|
</span>
|
||||||
|
<label v-if="label">{{ label }}</label>
|
||||||
|
</slot>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
initialActive: {type: Boolean, default: null},
|
||||||
|
el: {type: String, default: ""},
|
||||||
|
label: {type: String, default: ""},
|
||||||
|
icon: {type: String, default: "fa fa-bars"},
|
||||||
|
ariaLabel: {type: String, default: ""},
|
||||||
|
ariaDescription: {type: String, default: ""},
|
||||||
|
activeClass: {type: String, default:"active"},
|
||||||
|
/// switch toggle of all items of this group.
|
||||||
|
group: {type: String, default: ""},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
active: this.initialActive,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
groupClass() {
|
||||||
|
return this.group && "a-switch-" + this.group || ''
|
||||||
|
},
|
||||||
|
|
||||||
|
buttonClass() {
|
||||||
|
return [
|
||||||
|
this.active && 'active' || '',
|
||||||
|
this.groupClass
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggle() {
|
||||||
|
this.set(!this.active)
|
||||||
|
},
|
||||||
|
|
||||||
|
set(active) {
|
||||||
|
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)
|
||||||
|
el.__vnode.ctx.ctx.set(false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
if(this.initialActive !== null)
|
||||||
|
this.set(this.initialActive)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
288
radiocampus/assets/src/components/ATrackListEditor.vue
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
<template>
|
||||||
|
<div class="a-tracklist-editor">
|
||||||
|
<div class="flex-row">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<slot name="title" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-row align-right">
|
||||||
|
<div class="field has-addons">
|
||||||
|
<p class="control">
|
||||||
|
<button type="button" :class="['button','p-2', page == Page.Text ? 'is-primary' : 'is-light']"
|
||||||
|
@click="page = Page.Text">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<i class="fa fa-pencil"></i>
|
||||||
|
</span>
|
||||||
|
<span>{{ labels.text }}</span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<p class="control">
|
||||||
|
<button type="button" :class="['button','p-2', page == Page.List ? 'is-primary' : 'is-light']"
|
||||||
|
@click="page = Page.List">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<i class="fa fa-list"></i>
|
||||||
|
</span>
|
||||||
|
<span>{{ labels.list }}</span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<p class="control ml-3">
|
||||||
|
<button type="button" class="button is-info square"
|
||||||
|
:title="labels.settings"
|
||||||
|
@click="$refs.settings.open()">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<i class="fa fa-cog"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section v-show="page == Page.Text" class="panel">
|
||||||
|
<textarea ref="textarea" class="is-fullwidth is-size-6" rows="20"
|
||||||
|
@change="updateList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
<section v-show="page == Page.List" class="panel">
|
||||||
|
<a-form-set ref="formset"
|
||||||
|
:form-data="formData" :initials="initData.items"
|
||||||
|
:columnsOrderable="true" :labels="labels"
|
||||||
|
order-by="position"
|
||||||
|
@load="updateInput" @colmove="onColumnMove" @move="updateInput"
|
||||||
|
@cell="onCellEvent">
|
||||||
|
<template v-for="[name,slot] of rowsSlots" :key="slot"
|
||||||
|
v-slot:[slot]="data">
|
||||||
|
<slot v-if="name != 'row-tail'" :name="name" v-bind="data"/>
|
||||||
|
</template>
|
||||||
|
</a-form-set>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<a-modal ref="settings" :title="labels.settings">
|
||||||
|
<template #default>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" style="vertical-align: middle">
|
||||||
|
{{ labels.columns }}
|
||||||
|
</label>
|
||||||
|
<table class="table is-bordered"
|
||||||
|
style="vertical-align: middle">
|
||||||
|
<tr v-if="$refs.formset">
|
||||||
|
<a-row :columns="$refs.formset.rows.columnNames"
|
||||||
|
:item="$refs.formset.rows.columnLabels"
|
||||||
|
@move="$refs.formset.rows.moveColumn"
|
||||||
|
>
|
||||||
|
<template v-slot:cell-after="{cell}">
|
||||||
|
<td style="cursor:pointer;" v-if="cell.col < $refs.formset.rows.columns_.length-1">
|
||||||
|
<span class="icon" @click="$refs.formset.rows.moveColumn({from: cell.col, to: cell.col+1})"
|
||||||
|
><i class="fa fa-left-right"/>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</a-row>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-row">
|
||||||
|
<div class="field is-inline-block is-vcentered flex-grow-1">
|
||||||
|
<label class="label is-inline mr-2"
|
||||||
|
style="vertical-align: middle">
|
||||||
|
Séparateur</label>
|
||||||
|
<div class="control is-inline-block"
|
||||||
|
style="vertical-align: middle;">
|
||||||
|
<input type="text" ref="sep" class="input is-inline is-text-centered is-small"
|
||||||
|
style="max-width: 5em;"
|
||||||
|
v-model="separator" @change="updateList()"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex-row align-right">
|
||||||
|
<a-action-button icon="fa fa-floppy-disk"
|
||||||
|
v-if="settingsChanged"
|
||||||
|
class="button control p-2 mr-3 is-secondary" run-class="blink"
|
||||||
|
:url="settingsUrl" method="POST"
|
||||||
|
:data="settings"
|
||||||
|
:aria-label="labels.save_settings"
|
||||||
|
@done="settingsSaved()">
|
||||||
|
{{ labels.save_settings }}
|
||||||
|
</a-action-button>
|
||||||
|
<button class="button" type="button" @click="$refs.settings.close()">
|
||||||
|
Fermer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import {dropRightWhile, cloneDeep, isEqual} from 'lodash'
|
||||||
|
|
||||||
|
import AActionButton from './AActionButton'
|
||||||
|
import AFormSet from './AFormSet'
|
||||||
|
import ARow from './ARow'
|
||||||
|
import AModal from "./AModal"
|
||||||
|
|
||||||
|
/// Page display
|
||||||
|
export const Page = {
|
||||||
|
Text: 0, List: 1, Settings: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { AActionButton, AFormSet, ARow, AModal },
|
||||||
|
props: {
|
||||||
|
formData: Object,
|
||||||
|
labels: Object,
|
||||||
|
|
||||||
|
///! initial data as: {items: [], fields: {column_name: label, settings: {}}
|
||||||
|
initData: Object,
|
||||||
|
dataPrefix: String,
|
||||||
|
settingsUrl: String,
|
||||||
|
defaultColumns: {
|
||||||
|
type: Array,
|
||||||
|
default: () => ['artist', 'title', 'tags', 'album', 'year', 'timestamp']},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const settings = {
|
||||||
|
// tracklist_editor_columns: this.columns,
|
||||||
|
tracklist_editor_sep: ' -- ',
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
Page: Page,
|
||||||
|
page: Page.Text,
|
||||||
|
extraData: {},
|
||||||
|
settings,
|
||||||
|
savedSettings: cloneDeep(settings),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
rows() { return this.$refs.formset && this.$refs.formset.rows },
|
||||||
|
columns() { return this.rows && this.rows.columns_ || [] },
|
||||||
|
|
||||||
|
settingsChanged() {
|
||||||
|
var k = Object.keys(this.savedSettings)
|
||||||
|
.findIndex(k => !isEqual(this.settings[k], this.savedSettings[k]))
|
||||||
|
return k != -1
|
||||||
|
},
|
||||||
|
|
||||||
|
separator: {
|
||||||
|
set(value) {
|
||||||
|
this.settings.tracklist_editor_sep = value
|
||||||
|
if(this.page == Page.List)
|
||||||
|
this.updateInput()
|
||||||
|
},
|
||||||
|
get() { return this.settings.tracklist_editor_sep }
|
||||||
|
},
|
||||||
|
|
||||||
|
rowsSlots() {
|
||||||
|
return Object.keys(this.$slots)
|
||||||
|
.filter(x => x.startsWith('row-') || x.startsWith('rows-') || x.startsWith('control-'))
|
||||||
|
.map(x => [x, x.startsWith('rows-') ? x.slice(5) : x])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onCellEvent(event) {
|
||||||
|
switch(event.name) {
|
||||||
|
case 'change': this.updateInput();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onColumnMove() {
|
||||||
|
this.settings.tracklist_editor_columns = this.$refs.formset.rows.columnNames
|
||||||
|
if(this.page == this.Page.List)
|
||||||
|
this.updateInput()
|
||||||
|
else
|
||||||
|
this.updateList()
|
||||||
|
},
|
||||||
|
|
||||||
|
updateList() {
|
||||||
|
const items = this.toList(this.$refs.textarea.value)
|
||||||
|
this.$refs.formset.set.reset(items)
|
||||||
|
},
|
||||||
|
|
||||||
|
updateInput() {
|
||||||
|
const input = this.toText(this.$refs.formset.items)
|
||||||
|
this.$refs.textarea.value = input
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From input and separator, return list of items.
|
||||||
|
*/
|
||||||
|
toList(input) {
|
||||||
|
const columns = this.$refs.formset.rows.columns_
|
||||||
|
var lines = input.split('\n')
|
||||||
|
var items = []
|
||||||
|
|
||||||
|
for(let line of lines) {
|
||||||
|
line = line.trimLeft()
|
||||||
|
if(!line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var lineBits = line.split(this.separator)
|
||||||
|
var item = {}
|
||||||
|
for(var col in columns) {
|
||||||
|
if(col >= lineBits.length)
|
||||||
|
break
|
||||||
|
const column = columns[col]
|
||||||
|
item[column.name] = lineBits[col].trim()
|
||||||
|
}
|
||||||
|
item && items.push(item)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From items and separator return a string
|
||||||
|
*/
|
||||||
|
toText(items) {
|
||||||
|
const columns = this.$refs.formset.rows.columns_
|
||||||
|
const sep = ` ${this.separator.trim()} `
|
||||||
|
const lines = []
|
||||||
|
for(let item of items) {
|
||||||
|
if(!item)
|
||||||
|
continue
|
||||||
|
var line = []
|
||||||
|
for(var col of columns)
|
||||||
|
line.push(item.data[col.name] || '')
|
||||||
|
line = dropRightWhile(line, x => !x || !('' + x).trim())
|
||||||
|
line = line.join(sep).trimRight()
|
||||||
|
lines.push(line)
|
||||||
|
}
|
||||||
|
return lines.join('\n')
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
_data_key(key) {
|
||||||
|
key = key.slice(this.dataPrefix.length)
|
||||||
|
try {
|
||||||
|
var [index, attr] = key.split('-', 1)
|
||||||
|
return [Number(index), attr]
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
return [null, key]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//! Update saved settings from this.settings
|
||||||
|
settingsSaved(settings=null) {
|
||||||
|
if(settings !== null)
|
||||||
|
this.settings = settings
|
||||||
|
if(this.$refs.settings)
|
||||||
|
this.$refs.settings.close()
|
||||||
|
this.savedSettings = cloneDeep(this.settings)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
const settings = this.initData && this.initData.settings
|
||||||
|
if(settings) {
|
||||||
|
this.settingsSaved(settings)
|
||||||
|
this.rows.sortColumns(settings.tracklist_editor_columns)
|
||||||
|
}
|
||||||
|
this.page = this.initData.items.length ? Page.List : Page.Text
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
24
radiocampus/assets/src/components/admin.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import AFileUpload from "./AFileUpload.vue"
|
||||||
|
import ASelectFile from "./ASelectFile.vue"
|
||||||
|
import AStatistics from './AStatistics.vue'
|
||||||
|
import AStreamer from './AStreamer.vue'
|
||||||
|
|
||||||
|
import AFormSet from './AFormSet.vue'
|
||||||
|
import ATrackListEditor from './ATrackListEditor.vue'
|
||||||
|
import ASoundListEditor from './ASoundListEditor.vue'
|
||||||
|
import AEditor from './AEditor.vue'
|
||||||
|
|
||||||
|
import AManyToManyEdit from "./AManyToManyEdit.vue"
|
||||||
|
|
||||||
|
import base from "./index.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const admin = {
|
||||||
|
...base,
|
||||||
|
AManyToManyEdit,
|
||||||
|
AFileUpload, ASelectFile, AEditor,
|
||||||
|
AFormSet, ATrackListEditor, ASoundListEditor,
|
||||||
|
AStatistics, AStreamer,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default admin
|
26
radiocampus/assets/src/components/index.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import AAutocomplete from './AAutocomplete.vue'
|
||||||
|
import AModal from "./AModal.vue"
|
||||||
|
import AActionButton from './AActionButton.vue'
|
||||||
|
import ADropdown from "./ADropdown.vue"
|
||||||
|
import ACarousel from './ACarousel.vue'
|
||||||
|
import AEpisode from './AEpisode.vue'
|
||||||
|
import AList from './AList.vue'
|
||||||
|
import APage from './APage.vue'
|
||||||
|
import APlayer from './APlayer.vue'
|
||||||
|
import APlaylist from './APlaylist.vue'
|
||||||
|
import AProgress from './AProgress.vue'
|
||||||
|
import ASoundItem from './ASoundItem.vue'
|
||||||
|
import ASwitch from './ASwitch.vue'
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core components
|
||||||
|
*/
|
||||||
|
export const base = {
|
||||||
|
AActionButton, AAutocomplete, AModal,
|
||||||
|
ACarousel, ADropdown, AEpisode, AList, APage, APlayer, APlaylist,
|
||||||
|
AProgress, ASoundItem, ASwitch,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default base
|
84
radiocampus/assets/src/index.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* This module includes code available for both the public website and
|
||||||
|
* administration interface)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'vue'
|
||||||
|
|
||||||
|
//-- aircox
|
||||||
|
import App, {PlayerApp} from './app'
|
||||||
|
import VueLoader from './vueLoader'
|
||||||
|
import Sound from './sound'
|
||||||
|
import {Set} from './model'
|
||||||
|
|
||||||
|
import './styles/common.scss'
|
||||||
|
|
||||||
|
|
||||||
|
window.aircox = {
|
||||||
|
// main application
|
||||||
|
loader: null,
|
||||||
|
get app() { return this.loader.app },
|
||||||
|
|
||||||
|
// player application
|
||||||
|
playerLoader: null,
|
||||||
|
get playerApp() { return this.playerLoader && this.playerLoader.app },
|
||||||
|
get player() { return this.playerLoader.vm && this.playerLoader.vm.$refs.player },
|
||||||
|
|
||||||
|
Set, Sound,
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize main application and player.
|
||||||
|
*/
|
||||||
|
init(props=null, {hotReload=false, el=null,
|
||||||
|
config=null, playerConfig=null,
|
||||||
|
initApp=true, initPlayer=true,
|
||||||
|
loader=null, playerLoader=null}={})
|
||||||
|
{
|
||||||
|
if(initPlayer) {
|
||||||
|
playerConfig = playerConfig || PlayerApp
|
||||||
|
playerLoader = playerLoader || new VueLoader(playerConfig)
|
||||||
|
playerLoader.enable(false)
|
||||||
|
this.playerLoader = playerLoader
|
||||||
|
|
||||||
|
document.addEventListener("keyup", e => this.onKeyPress(e), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(initApp) {
|
||||||
|
config = config || window.App || App
|
||||||
|
config.el = el || config.el
|
||||||
|
loader = loader || new VueLoader({el, props, ...config})
|
||||||
|
loader.enable(hotReload)
|
||||||
|
this.loader = loader
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyPress(/*event*/) {
|
||||||
|
/*
|
||||||
|
if(event.key == " ") {
|
||||||
|
this.player.togglePlay()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter navbar dropdown menu items
|
||||||
|
*/
|
||||||
|
filter_menu(event) {
|
||||||
|
var filter = new RegExp(event.target.value, 'gi');
|
||||||
|
var container = event.target.closest('.navbar-dropdown');
|
||||||
|
|
||||||
|
if(event.target.value)
|
||||||
|
for(let item of container.querySelectorAll('a.navbar-item'))
|
||||||
|
item.style.display = item.innerHTML.search(filter) == -1 ? 'none' : null;
|
||||||
|
else
|
||||||
|
for(let item of container.querySelectorAll('a.navbar-item'))
|
||||||
|
item.style.display = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
pickDate(url, date) {
|
||||||
|
url = `${url}?date=${date.id}`
|
||||||
|
this.loader.pageLoad.load(url)
|
||||||
|
}
|
||||||
|
}
|
83
radiocampus/assets/src/live.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import {setEcoInterval} from './utils';
|
||||||
|
import Model from './model';
|
||||||
|
|
||||||
|
export default class Live {
|
||||||
|
constructor({url,timeout=10,src=""}={}) {
|
||||||
|
this.url = url;
|
||||||
|
this.timeout = timeout;
|
||||||
|
this.src = src;
|
||||||
|
|
||||||
|
this.interval = null
|
||||||
|
this.promise = null
|
||||||
|
this.items = []
|
||||||
|
this.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
//-- data refreshing
|
||||||
|
drop() {
|
||||||
|
this.promise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data from server.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {Function} options.then: call this method on fetch, `this` passed as argument.
|
||||||
|
* @return {Promise} Promise resolving to fetched items.
|
||||||
|
*/
|
||||||
|
fetch({then=null}={}) {
|
||||||
|
const promise = fetch(this.url).then(response =>
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
this.items = data
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
let item = data.find(it => it.start && (it.start <= now < it.end)) ||
|
||||||
|
data.length ? data[0] : null;
|
||||||
|
if(item) {
|
||||||
|
item.src = this.src
|
||||||
|
this.current = new Model(item)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
this.current = null
|
||||||
|
if(then)
|
||||||
|
then(this)
|
||||||
|
return this.items
|
||||||
|
})
|
||||||
|
|
||||||
|
this.promise = promise;
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
_refresh(options={}) {
|
||||||
|
const promise = this.fetch(options);
|
||||||
|
promise.then(() => {
|
||||||
|
if(promise != this.promise)
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh live info every `this.timeout`.
|
||||||
|
* @param {Object} options: arguments passed to `this.fetch`.
|
||||||
|
*/
|
||||||
|
refresh(options={}) {
|
||||||
|
if(this.interval !== null)
|
||||||
|
return
|
||||||
|
|
||||||
|
this._refresh(options)
|
||||||
|
this.interval = setEcoInterval(() => this._refresh(options), this.timeout*1000)
|
||||||
|
return this.interval
|
||||||
|
}
|
||||||
|
|
||||||
|
stopRefresh() {
|
||||||
|
this.interval !== null && clearInterval(this.interval)
|
||||||
|
}
|
||||||
|
}
|
371
radiocampus/assets/src/model.js
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Return cookie with provided key
|
||||||
|
*/
|
||||||
|
function getCookie(key) {
|
||||||
|
if(document.cookie && document.cookie !== '') {
|
||||||
|
const cookie = document.cookie.split(';')
|
||||||
|
.find(c => c.trim().startsWith(key + '='))
|
||||||
|
return cookie ? decodeURIComponent(cookie.split('=')[1]) : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSRF token provided by Django
|
||||||
|
*/
|
||||||
|
var csrfToken = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CSRF token
|
||||||
|
*/
|
||||||
|
export function getCsrf() {
|
||||||
|
if(csrfToken === null)
|
||||||
|
csrfToken = getCookie('csrftoken')
|
||||||
|
return csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: prevent duplicate simple fetch
|
||||||
|
/**
|
||||||
|
* Provide interface used to fetch and manipulate objects.
|
||||||
|
*/
|
||||||
|
export default class Model {
|
||||||
|
/**
|
||||||
|
* Instanciate model with provided data and options.
|
||||||
|
* By default `url` is taken from `data.url_`.
|
||||||
|
*/
|
||||||
|
constructor(data={}, {url=null, ...options}={}) {
|
||||||
|
this.url = url || data.url_;
|
||||||
|
this.options = options;
|
||||||
|
this.commit(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
get created() { return !this.id }
|
||||||
|
get errors() { return this.data && this.data.__errors__ }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get instance id from its data
|
||||||
|
*/
|
||||||
|
static getId(data) {
|
||||||
|
return 'id' in data ? data.id : data.pk;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return fetch options
|
||||||
|
*/
|
||||||
|
static getOptions(options) {
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-CSRFToken': getCsrf(),
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return model instances for the provided list of model data.
|
||||||
|
* @param {Array} items: array of data
|
||||||
|
* @param {Object} options: options passed down to all model instances
|
||||||
|
*/
|
||||||
|
static fromList(items, options={}) {
|
||||||
|
return items ? items.map(d => new this(d, options)) : []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch item from server
|
||||||
|
*/
|
||||||
|
static fetch(url, {many=false, ...options}={}, args={}) {
|
||||||
|
options = this.getOptions(options)
|
||||||
|
const request = fetch(url, options).then(response => response.json());
|
||||||
|
if(many)
|
||||||
|
return request.then(data => {
|
||||||
|
if(!(data instanceof Array))
|
||||||
|
data = data.results
|
||||||
|
return this.fromList(data, args)
|
||||||
|
})
|
||||||
|
else
|
||||||
|
return request.then(data => new this(data, {url: url, ...args}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data from server.
|
||||||
|
*/
|
||||||
|
fetch(options) {
|
||||||
|
options = this.constructor.getOptions(options)
|
||||||
|
return fetch(this.url, options)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => this.commit(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call API action on object.
|
||||||
|
*/
|
||||||
|
action(path, options, commit=false) {
|
||||||
|
options = this.constructor.getOptions(options)
|
||||||
|
const promise = fetch(this.url + path, options);
|
||||||
|
return commit ? promise.then(data => data.json())
|
||||||
|
.then(data => { this.commit(data); this.data })
|
||||||
|
: promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set instance's data with provided data. Return None
|
||||||
|
*/
|
||||||
|
commit(data) {
|
||||||
|
this.data = data;
|
||||||
|
this.id = this.constructor.getId(this.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update model data, without reset previous value.
|
||||||
|
* Item is marked as updated.
|
||||||
|
*/
|
||||||
|
update(data) {
|
||||||
|
this.data = {...this.data, ...data}
|
||||||
|
this.id = this.constructor.getId(this.data)
|
||||||
|
this.updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
this.deleted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save instance into localStorage.
|
||||||
|
*/
|
||||||
|
store(key) {
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(this.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load model instance from localStorage.
|
||||||
|
*/
|
||||||
|
static storeLoad(key) {
|
||||||
|
let item = window.localStorage.getItem(key);
|
||||||
|
return item === null ? item : new this(JSON.parse(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if model instance has no data
|
||||||
|
*/
|
||||||
|
get isEmpty() {
|
||||||
|
return !this.data || Object.keys(this.data).findIndex(k => !!this.data[k] && this.data[k] !== 0) == -1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return error for a specific attribute name if any
|
||||||
|
*/
|
||||||
|
error(attr=null) {
|
||||||
|
return attr === null ? this.errors : this.errors && this.errors[attr]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of models
|
||||||
|
*/
|
||||||
|
export class Set {
|
||||||
|
constructor(model, {items=[],url=null,args={},unique=null,max=null,storeKey=null}={}) {
|
||||||
|
this.items = [];
|
||||||
|
this.model = model;
|
||||||
|
this.url = url;
|
||||||
|
this.unique = unique;
|
||||||
|
this.max = max;
|
||||||
|
this.storeKey = storeKey;
|
||||||
|
|
||||||
|
for(var item of items)
|
||||||
|
this.push(item, {args: args, save: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Return total items count
|
||||||
|
get length() { return this.items.length }
|
||||||
|
|
||||||
|
//! Return a list of items marked as deleted
|
||||||
|
get deletedItems() {
|
||||||
|
return this.items.filter(i => i.deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Return a list of created items
|
||||||
|
get createdItems() {
|
||||||
|
return this.items.filter(i => !i.deleted && !i.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Return a list of updated items
|
||||||
|
get updatedItems() {
|
||||||
|
return this.items.filter(i => i.updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch multiple items from server
|
||||||
|
*/
|
||||||
|
static fetch(model, url, options=null, args=null) {
|
||||||
|
options = model.getOptions(options)
|
||||||
|
return fetch(url, options)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => (data instanceof Array ? data : data.results)
|
||||||
|
.map(d => new model(d, {url: url, ...args})))
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch({url=null, reset=false, ...options}={}, args=null) {
|
||||||
|
url = url || this.url
|
||||||
|
options = this.model.getOptions(options)
|
||||||
|
return fetch(url, options)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data =>
|
||||||
|
(data instanceof Array ? data : data.results)
|
||||||
|
.map(d => new this.model(d, {url: url, ...args}))
|
||||||
|
)
|
||||||
|
.then(data => {
|
||||||
|
if(reset)
|
||||||
|
this.items = data
|
||||||
|
else
|
||||||
|
// TODO: remove duplicate
|
||||||
|
this.items = [...this.items, ...data]
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit changes to server.
|
||||||
|
* py-ref: `views.mixin.ListCommitMixin`
|
||||||
|
*/
|
||||||
|
commit(url, {getData=null, fields=null, ...options}={}) {
|
||||||
|
if(!getData && fields)
|
||||||
|
getData = (i) => fields.reduce((r, f) => {
|
||||||
|
r[f] = i.data[f]
|
||||||
|
return r
|
||||||
|
}, {})
|
||||||
|
const createdItems = this.createdItems
|
||||||
|
const body = {
|
||||||
|
delete: this.deletedItems.map(i => i.id),
|
||||||
|
update: this.updatedItems.map(getData),
|
||||||
|
create: createdItems.map(getData),
|
||||||
|
}
|
||||||
|
if(!body.delete && !body.update && !body.create)
|
||||||
|
return
|
||||||
|
|
||||||
|
getData = getData || ((i) => i.data);
|
||||||
|
options = this.model.getOptions(options)
|
||||||
|
options.method = "POST"
|
||||||
|
options.body = JSON.stringify(body)
|
||||||
|
return fetch(url, options)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const {created, updated, deleted} = data
|
||||||
|
if(createdItems)
|
||||||
|
this.items = this.items.filter(i => createdItems.indexOf(i) == -1)
|
||||||
|
if(deleted)
|
||||||
|
this.items = this.items.filter(i => deleted.indexOf(i.id) == -1)
|
||||||
|
|
||||||
|
this.extend(created)
|
||||||
|
this.extend(updated)
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load list from localStorage
|
||||||
|
*/
|
||||||
|
static storeLoad(model, key, args={}) {
|
||||||
|
let items = window.localStorage.getItem(key);
|
||||||
|
return new this(model, {...args, storeKey: key, items: items ? JSON.parse(items) : []});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store list into localStorage
|
||||||
|
*/
|
||||||
|
store() {
|
||||||
|
this.storeKey && window.localStorage.setItem(this.storeKey, JSON.stringify(
|
||||||
|
this.items.map(i => i.data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save item
|
||||||
|
*/
|
||||||
|
save() {
|
||||||
|
this.storeKey && this.store();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get item at index
|
||||||
|
*/
|
||||||
|
get(index) { return this.items[index] }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find an item by id or using a predicate function
|
||||||
|
*/
|
||||||
|
find(pred) {
|
||||||
|
return pred instanceof Function ? this.items.find(pred)
|
||||||
|
: this.items.find(x => x.id == pred.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find item index by id or using a predicate function
|
||||||
|
*/
|
||||||
|
findIndex(pred) {
|
||||||
|
return pred instanceof Function ? this.items.findIndex(pred)
|
||||||
|
: this.items.findIndex(x => x.id == pred.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(items, options) {
|
||||||
|
items.forEach(i => this.push(i, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add item to set, return index.
|
||||||
|
* If item already exists, replace it.
|
||||||
|
*/
|
||||||
|
push(item, {args={},save=true}={}) {
|
||||||
|
item = item instanceof this.model ? item : new this.model(item, args);
|
||||||
|
let index = -1
|
||||||
|
if(this.unique && item.id) {
|
||||||
|
index = this.findIndex(item);
|
||||||
|
if(index > -1)
|
||||||
|
this.items[index] = item
|
||||||
|
}
|
||||||
|
if(index == -1) {
|
||||||
|
if(this.max && this.items.length >= this.max)
|
||||||
|
this.items.splice(0,this.items.length-this.max)
|
||||||
|
this.items.push(item)
|
||||||
|
index = this.items.length-1
|
||||||
|
}
|
||||||
|
save && this.save()
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove item from set by index
|
||||||
|
*/
|
||||||
|
remove(index, {save=true}={}) {
|
||||||
|
this.items.splice(index,1);
|
||||||
|
save && this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear items, assign new ones
|
||||||
|
*/
|
||||||
|
reset(items=[]) {
|
||||||
|
// TODO: check reactivity
|
||||||
|
this.items = []
|
||||||
|
for(var item of items)
|
||||||
|
this.push(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
move(from, to) {
|
||||||
|
if(from >= this.length || to > this.length)
|
||||||
|
throw "source or target index is not in range"
|
||||||
|
|
||||||
|
const value = this.items[from]
|
||||||
|
this.items.splice(from, 1)
|
||||||
|
this.items.splice(to, 0, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Set[Symbol.iterator] = function () {
|
||||||
|
return this.items[Symbol.iterator]();
|
||||||
|
}
|
179
radiocampus/assets/src/pageLoad.js
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Load page without leaving current one (hot-reload).
|
||||||
|
*/
|
||||||
|
export default class PageLoad {
|
||||||
|
constructor(el, {loadingClass="loading", append=false}={}) {
|
||||||
|
this.el = el
|
||||||
|
this.append = append
|
||||||
|
this.loadingClass = loadingClass
|
||||||
|
}
|
||||||
|
|
||||||
|
get target() {
|
||||||
|
if(!this._target)
|
||||||
|
this._target = document.querySelector(this.el)
|
||||||
|
return this._target
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this._target = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable hot reload: catch page change in order to fetch them and
|
||||||
|
* load page without actually leaving current one.
|
||||||
|
*/
|
||||||
|
enable(target=null) {
|
||||||
|
if(this._pageChanged)
|
||||||
|
throw "Already enabled, please disable me"
|
||||||
|
|
||||||
|
if(!target)
|
||||||
|
target = this.target || document.body
|
||||||
|
this.historySave(document.location, true)
|
||||||
|
|
||||||
|
this._pageChanged = event => this.pageChanged(event)
|
||||||
|
this._statePopped = event => this.statePopped(event)
|
||||||
|
|
||||||
|
target.addEventListener('click', this._pageChanged, true)
|
||||||
|
target.addEventListener('submit', this._pageChanged, true)
|
||||||
|
window.addEventListener('popstate', this._statePopped, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable hot reload, remove listeners.
|
||||||
|
*/
|
||||||
|
disable() {
|
||||||
|
this.target.removeEventListener('click', this._pageChanged, true)
|
||||||
|
this.target.removeEventListener('submit', this._pageChanged, true)
|
||||||
|
window.removeEventListener('popstate', this._statePopped, true)
|
||||||
|
|
||||||
|
this._pageChanged = null
|
||||||
|
this._statePopped = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch url, return promise, similar to standard Fetch API.
|
||||||
|
* Default implementation just forward argument to it.
|
||||||
|
*/
|
||||||
|
fetch(url, options) {
|
||||||
|
return fetch(url, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch app from remote and mount application.
|
||||||
|
*/
|
||||||
|
load(url, {mount=true, scroll=[0,0], ...options}={}) {
|
||||||
|
if(this.loadingClass)
|
||||||
|
this.target.classList.add(this.loadingClass)
|
||||||
|
|
||||||
|
if(this.onLoad)
|
||||||
|
this.onLoad({url, el: this.el, options})
|
||||||
|
if(scroll)
|
||||||
|
window.scroll(...scroll)
|
||||||
|
return this.fetch(url, options).then(response => response.text())
|
||||||
|
.then(content => {
|
||||||
|
if(this.loadingClass)
|
||||||
|
this.target.classList.remove(this.loadingClass)
|
||||||
|
|
||||||
|
var doc = new DOMParser().parseFromString(content, 'text/html')
|
||||||
|
var dom = doc.querySelectorAll(this.el)
|
||||||
|
var result = {url,
|
||||||
|
content: dom || [document.createTextNode(content)],
|
||||||
|
title: doc.title,
|
||||||
|
append: this.append}
|
||||||
|
mount && this.mount(result)
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mount the page on provided target element
|
||||||
|
*/
|
||||||
|
mount({content, title=null, ...options}={}) {
|
||||||
|
if(this.onPreMount)
|
||||||
|
this.onPreMount({target: this.target, content, items, title})
|
||||||
|
var items = null;
|
||||||
|
if(content)
|
||||||
|
items = this.mountContent(content, options)
|
||||||
|
if(title)
|
||||||
|
document.title = title
|
||||||
|
if(this.onMount)
|
||||||
|
this.onMount({target: this.target, content, items, title})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mount page content
|
||||||
|
*/
|
||||||
|
mountContent(content, {append=false}={}) {
|
||||||
|
if(typeof content == "string") {
|
||||||
|
this.target.innerHTML = append ? this.target.innerHTML + content
|
||||||
|
: content;
|
||||||
|
// TODO
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!append)
|
||||||
|
this.target.innerHTML = ""
|
||||||
|
|
||||||
|
var fragment = document.createDocumentFragment()
|
||||||
|
var items = []
|
||||||
|
for(var node of content)
|
||||||
|
while(node.firstChild) {
|
||||||
|
items.push(node.firstChild)
|
||||||
|
fragment.appendChild(node.firstChild)
|
||||||
|
}
|
||||||
|
this.target.append(fragment)
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save application state into browser history
|
||||||
|
historySave(url,replace=false) {
|
||||||
|
const state = { content: this.target.innerHTML,
|
||||||
|
title: document.title, }
|
||||||
|
if(replace)
|
||||||
|
history.replaceState(state, '', url)
|
||||||
|
else
|
||||||
|
history.pushState(state, '', url)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchPageLoaded(url) {
|
||||||
|
var evt = new CustomEvent("pageLoaded", {detail: url})
|
||||||
|
document.dispatchEvent(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- events
|
||||||
|
pageChanged(event) {
|
||||||
|
let submit = event.type == 'submit';
|
||||||
|
let target = submit || event.target.tagName == 'A'
|
||||||
|
? event.target : event.target.closest('a');
|
||||||
|
if(!target || target.hasAttribute('target') || (target.dataset && target.dataset.forceReload))
|
||||||
|
return;
|
||||||
|
|
||||||
|
let url = submit ? target.getAttribute('action') || ''
|
||||||
|
: target.getAttribute('href');
|
||||||
|
let domain = window.location.protocol + '//' + window.location.hostname
|
||||||
|
let stay = (url === '' || url.startsWith('/') || url.startsWith('?') ||
|
||||||
|
url.startsWith(domain)) && url.indexOf('wp-admin') == -1
|
||||||
|
if(url===null || !stay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = {};
|
||||||
|
if(submit) {
|
||||||
|
let formData = new FormData(event.target);
|
||||||
|
if(target.method == 'get')
|
||||||
|
url += '?' + (new URLSearchParams(formData)).toString();
|
||||||
|
else
|
||||||
|
options = {...options, method: target.method, body: formData}
|
||||||
|
}
|
||||||
|
this.load(url, options).then(() => this.dispatchPageLoaded(url)).then(() => this.historySave(url))
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
statePopped(event) {
|
||||||
|
const state = event.state
|
||||||
|
if(state && state.content)
|
||||||
|
this.mount({ content: state.content, title: state.title });
|
||||||
|
}
|
||||||
|
}
|
5
radiocampus/assets/src/public.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import "./styles/public.scss"
|
||||||
|
import './index.js'
|
||||||
|
import App from './app.js'
|
||||||
|
|
||||||
|
window.App = App
|
12
radiocampus/assets/src/sound.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import Model from './model';
|
||||||
|
|
||||||
|
|
||||||
|
export default class Sound extends Model {
|
||||||
|
constructor({sound={}, ...data}={}, options={}) {
|
||||||
|
// flatten EpisodeSound and sound data
|
||||||
|
super({...sound, ...data}, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() { return this.data.name }
|
||||||
|
get src() { return this.data.url }
|
||||||
|
}
|
98
radiocampus/assets/src/streamer.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import Model from './model';
|
||||||
|
import {setEcoInterval} from './utils';
|
||||||
|
|
||||||
|
|
||||||
|
export class Streamer extends Model {
|
||||||
|
get playlists() { return this.data ? this.data.playlists : []; }
|
||||||
|
get queues() { return this.data ? this.data.queues : []; }
|
||||||
|
get sources() { return [...this.queues, ...this.playlists]; }
|
||||||
|
get source() { return this.sources.find(o => o.id == this.data.source) }
|
||||||
|
|
||||||
|
commit(data) {
|
||||||
|
if(!this.data)
|
||||||
|
this.data = { id: data.id, playlists: [], queues: [] }
|
||||||
|
|
||||||
|
data.playlists = Playlist.fromList(data.playlists, {streamer: this});
|
||||||
|
data.queues = Queue.fromList(data.queues, {streamer: this});
|
||||||
|
super.commit(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Streamer;
|
||||||
|
|
||||||
|
export class Request extends Model {
|
||||||
|
static getId(data) { return data.rid; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Source extends Model {
|
||||||
|
constructor(data, {streamer=null, ...options}={}) {
|
||||||
|
super(data, options);
|
||||||
|
this.streamer = streamer;
|
||||||
|
setEcoInterval(() => this.tick(), 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
get isQueue() { return false; }
|
||||||
|
get isPlaylist() { return false; }
|
||||||
|
get isPlaying() { return this.data.status == 'playing' }
|
||||||
|
get isPaused() { return this.data.status == 'paused' }
|
||||||
|
|
||||||
|
get remainingString() {
|
||||||
|
if(!this.remaining)
|
||||||
|
return '00:00';
|
||||||
|
|
||||||
|
const seconds = Math.floor(this.remaining % 60);
|
||||||
|
const minutes = Math.floor(this.remaining / 60);
|
||||||
|
return String(minutes).padStart(2, '0') + ':' +
|
||||||
|
String(seconds).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
sync() { return this.action('sync/', {method: 'POST'}, true); }
|
||||||
|
skip() { return this.action('skip/', {method: 'POST'}, true); }
|
||||||
|
restart() { return this.action('restart/', {method: 'POST'}, true); }
|
||||||
|
|
||||||
|
seek(count) {
|
||||||
|
return this.action('seek/', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({count: count})
|
||||||
|
}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
tick() {
|
||||||
|
if(!this.data.remaining || !this.isPlaying)
|
||||||
|
return;
|
||||||
|
const delta = (Date.now() - this.commitDate) / 1000;
|
||||||
|
this.remaining = this.data.remaining - delta
|
||||||
|
}
|
||||||
|
|
||||||
|
commit(data) {
|
||||||
|
if(data.air_time)
|
||||||
|
data.air_time = new Date(data.air_time);
|
||||||
|
|
||||||
|
this.commitDate = Date.now()
|
||||||
|
super.commit(data)
|
||||||
|
this.remaining = data.remaining
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class Playlist extends Source {
|
||||||
|
get isPlaylist() { return true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class Queue extends Source {
|
||||||
|
get isQueue() { return true; }
|
||||||
|
get queue() { return this.data && this.data.queue; }
|
||||||
|
|
||||||
|
commit(data) {
|
||||||
|
data.queue = Request.fromList(data.queue);
|
||||||
|
super.commit(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
push(soundId) {
|
||||||
|
return this.action('push/', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({'sound_id': parseInt(soundId)})
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
}
|
58
radiocampus/assets/src/streamer/app.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import AdminApp from '../admin';
|
||||||
|
import Model from '../model';
|
||||||
|
import Sound from '../sound';
|
||||||
|
import {setEcoInterval} from '../utils';
|
||||||
|
|
||||||
|
import {Streamer, Queue} from './controllers';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...AdminApp,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
...(AdminApp.props || {}),
|
||||||
|
apiUrl: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// current streamer
|
||||||
|
streamer: null,
|
||||||
|
// all streamers
|
||||||
|
streamers: [],
|
||||||
|
// fetch interval id
|
||||||
|
fetchInterval: null,
|
||||||
|
Sound: Sound,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...(AdminApp.computed || {}),
|
||||||
|
|
||||||
|
sources() {
|
||||||
|
var sources = this.streamer ? this.streamer.sources : [];
|
||||||
|
return sources.filter(s => s.data)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
...(AdminApp.methods || {}),
|
||||||
|
|
||||||
|
fetchStreamers() {
|
||||||
|
Streamer.fetch(this.apiUrl, {many:true}).then(streamers => {
|
||||||
|
this.streamers = streamers
|
||||||
|
this.streamer = streamers ? streamers[0] : null
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.fetchStreamers();
|
||||||
|
this.fetchInterval = setEcoInterval(() => this.streamer && this.streamer.fetch(), 5000)
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyed() {
|
||||||
|
if(this.fetchInterval !== null)
|
||||||
|
clearInterval(this.fetchInterval)
|
||||||
|
}
|
||||||
|
}
|
58
radiocampus/assets/src/streamer/index.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<slot :streamer="streamer" :streamers="streamers" :Sound="Sound"
|
||||||
|
:sources="sources" :fetchStreamers="fetchStreamers"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import AdminApp from '../admin';
|
||||||
|
import Sound from '../sound';
|
||||||
|
import {setEcoInterval} from '../utils';
|
||||||
|
|
||||||
|
import {Streamer} from './controllers';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
apiUrl: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// current streamer
|
||||||
|
streamer: null,
|
||||||
|
// all streamers
|
||||||
|
streamers: [],
|
||||||
|
// fetch interval id
|
||||||
|
fetchInterval: null,
|
||||||
|
Sound: Sound,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
sources() {
|
||||||
|
var sources = this.streamer ? this.streamer.sources : [];
|
||||||
|
return sources.filter(s => s.data)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
fetchStreamers() {
|
||||||
|
Streamer.fetch(this.apiUrl, {many:true}).then(streamers => {
|
||||||
|
this.streamers = streamers
|
||||||
|
this.streamer = streamers ? streamers[0] : null
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.fetchStreamers();
|
||||||
|
this.fetchInterval = setEcoInterval(() => this.streamer && this.streamer.fetch(), 5000)
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyed() {
|
||||||
|
if(this.fetchInterval !== null)
|
||||||
|
clearInterval(this.fetchInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
1
radiocampus/assets/src/styles/*
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../../../assets/src/styles/*
|
101
radiocampus/assets/src/styles/admin.scss
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
@use "./vars";
|
||||||
|
@use "./components";
|
||||||
|
|
||||||
|
@import "bulma/sass/utilities/_all.sass";
|
||||||
|
@import "bulma/sass/elements/button";
|
||||||
|
@import "bulma/sass/components/navbar";
|
||||||
|
|
||||||
|
|
||||||
|
// enforce button usage inside custom application
|
||||||
|
#player, .ax {
|
||||||
|
@include components.button;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.admin {
|
||||||
|
.navbar.has-shadow, .navbar.is-fixed-bottom.has-shadow {
|
||||||
|
box-shadow: 0em 0em 1em rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
a.navbar-item.is-active {
|
||||||
|
border-bottom: 1px grey solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
& + .container {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-dropdown {
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-split {
|
||||||
|
margin: 0.2em 0em;
|
||||||
|
margin-right: 1em;
|
||||||
|
padding-right: 1em;
|
||||||
|
border-right: 1px vars.$grey-light solid;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin: 0em;
|
||||||
|
padding: 0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toolbar {
|
||||||
|
margin: 1em 0em;
|
||||||
|
background-color: transparent;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
padding-right: 2em;
|
||||||
|
margin-right: 1em;
|
||||||
|
border-right: 1px vars.$grey-light solid;
|
||||||
|
|
||||||
|
font-size: vars.$text-size;
|
||||||
|
font-weight: vars.$weight-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-dropdown {
|
||||||
|
max-height: 40rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
input {
|
||||||
|
z-index: 10000;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-brand {
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-brand img {
|
||||||
|
margin: 0em 0.4em;
|
||||||
|
margin-top: 0.3em;
|
||||||
|
max-height: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results > #result_list {
|
||||||
|
width: 100%;
|
||||||
|
margin: 1em 0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ul.menu-list li {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row a.deletelink {
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
}
|
98
radiocampus/assets/src/styles/common.scss
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
@use "./vars" as v;
|
||||||
|
@import "./vendor";
|
||||||
|
@import "./helpers";
|
||||||
|
|
||||||
|
//-- helpers/modifiers
|
||||||
|
//-- forms
|
||||||
|
input.half-field:not(:active):not(:hover) {
|
||||||
|
border: none;
|
||||||
|
background-color: rgba(0,0,0,0);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-- general
|
||||||
|
:root {
|
||||||
|
--body-bg: #fff;
|
||||||
|
--text-color: black;
|
||||||
|
--text-color-light: #555;
|
||||||
|
--break-color: rgb(225, 225, 225, 0.8);
|
||||||
|
|
||||||
|
--main-color: #EFCA08;
|
||||||
|
--main-color-light: #F4da51;
|
||||||
|
--main-color-dark: #F49F0A;
|
||||||
|
--secondary-color: #00A6A6;
|
||||||
|
--secondary-color-light: #4cc0c0;
|
||||||
|
--secondary-color-dark: #007ba8;
|
||||||
|
|
||||||
|
--disabled-color: #aaa;
|
||||||
|
--disabled-bg: #eee;
|
||||||
|
--link-fg: #00A6A6;
|
||||||
|
--link-hv-fg: var(--text-color);
|
||||||
|
|
||||||
|
--nav-primary-height: 3rem;
|
||||||
|
--nav-secondary-height: 2.5rem;
|
||||||
|
--nav-fg: var(--text-color);
|
||||||
|
--nav-bg: var(--main-color);
|
||||||
|
--nav-secondary-bg: var(--main-color-light);
|
||||||
|
--nav-hv-fg: var(--button-hv-fg);
|
||||||
|
--nav-hv-bg: var(--button-hv-bg);
|
||||||
|
--nav-active-fg: var(--button-active-fg);
|
||||||
|
--nav-active-bg: var(--button-active-bg);
|
||||||
|
--nav-fs: 1rem;
|
||||||
|
--nav-2-fs: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
html { font-size: 16px !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: v.$screen-wider) {
|
||||||
|
html { font-size: 20px !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: v.$screen-wider) {
|
||||||
|
html { font-size: 20px !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6, .heading, .title, .subtitle {
|
||||||
|
font-family: var(--heading-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.container:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-cover {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.modal .dropdown-menu {
|
||||||
|
z-index: 50,
|
||||||
|
}
|
782
radiocampus/assets/src/styles/components.scss
Normal file
@ -0,0 +1,782 @@
|
|||||||
|
@use "vars" as v;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--title-1-sz: 1.4rem;
|
||||||
|
--title-2-sz: 1.3rem;
|
||||||
|
--title-3-sz: 1.1rem;
|
||||||
|
--title-4-sz: 1.0rem;
|
||||||
|
--subtitle-1-sz: 1.6rem;
|
||||||
|
--subtitle-2-sz: 1.4rem;
|
||||||
|
--subtitle-3-sz: 1.2rem;
|
||||||
|
|
||||||
|
--heading-font-family: default;
|
||||||
|
--heading-bg: var(--main-color);
|
||||||
|
--heading-fg: var(--text-color);
|
||||||
|
--heading-hg-fg: var(--text-color);
|
||||||
|
--heading-hg-bg: var(--secondary-color);
|
||||||
|
--heading-link-hv-fg: var(--link-fg);
|
||||||
|
|
||||||
|
--cover-w: 10rem;
|
||||||
|
--cover-h: 10rem;
|
||||||
|
--cover-small-w: 10rem;
|
||||||
|
--cover-small-h: 10rem;
|
||||||
|
--cover-tiny-w: 10rem;
|
||||||
|
--cover-tiny-h: 10rem;
|
||||||
|
|
||||||
|
--card-w: var(--cover-w);
|
||||||
|
--preview-bg: var(--body-bg);
|
||||||
|
--preview-title-sz: var(--title-4-sz);
|
||||||
|
--preview-subtitle-sz: var(--title-4-sz);
|
||||||
|
--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};
|
||||||
|
--a-carousel-ml: calc(#{v.$mp-4} - 0.5rem);
|
||||||
|
--a-carousel-gap: #{v.$mp-4};
|
||||||
|
--a-carousel-nav-x: -#{v.$mp-3e};
|
||||||
|
--a-carousel-bg: none; // var(--secondary-color-light);
|
||||||
|
|
||||||
|
--a-progress-bg: transparent;
|
||||||
|
--a-progress-bar-bg: var(--secondary-color);
|
||||||
|
--a-progress-bar-color: var(--text-color);
|
||||||
|
--a-progress-bar-pd: #{v.$mp-2};
|
||||||
|
|
||||||
|
--a-playlist-header-bg: var(--secondary-color);
|
||||||
|
--a-playlist-header-fg: var(--text-color);
|
||||||
|
--a-playlist-title-sz: #{v.$text-size};
|
||||||
|
--a-playlist-title-pd: #{v.$mp-3};
|
||||||
|
--a-playlist-item-border: 1px var(--secondary-color) solid;
|
||||||
|
|
||||||
|
--a-sound-bg: var(--main-color);
|
||||||
|
--a-sound-hv-bg: var(--main-color);
|
||||||
|
--a-sound-hv-fg: var(--secondary-color);
|
||||||
|
--a-sound-playing-fg: var(--secondary-color-dark);
|
||||||
|
--a-sound-text-sz: #{v.$text-size};
|
||||||
|
|
||||||
|
--a-player-url-fg: var(--text-color);
|
||||||
|
--a-player-panel-bg: var(--main-color);
|
||||||
|
--a-player-bar-height: var(--nav-primary-height);
|
||||||
|
--a-player-bar-bg: var(--main-color);
|
||||||
|
--a-player-bar-title-alone-sz: #{v.$text-size-medium};
|
||||||
|
--a-player-bar-button-fg: var(--button-fg);
|
||||||
|
--a-player-bar-button-fg: var(--button-bg);
|
||||||
|
--a-player-bar-button-hv-fg: var(--button-hv-fg);
|
||||||
|
--a-player-bar-button-hv-bg: var(--button-hv-bg);
|
||||||
|
|
||||||
|
--button-fg: var(--text-color);
|
||||||
|
--button-bg: var(--main-color);
|
||||||
|
--button-sec-bg: var(--main-color-light);
|
||||||
|
--button-hv-fg: var(--text-color);
|
||||||
|
--button-hv-bg: var(--secondary-color-light);
|
||||||
|
--button-active-fg: var(--text-color);
|
||||||
|
--button-active-bg: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (max-width: v.$screen-wide) {
|
||||||
|
:root {
|
||||||
|
--cover-w: 10rem;
|
||||||
|
--cover-h: 10rem;
|
||||||
|
--cover-small-w: 6rem;
|
||||||
|
--cover-small-h: 6rem;
|
||||||
|
--cover-tiny-w: 4rem;
|
||||||
|
--cover-tiny-h: 4rem;
|
||||||
|
|
||||||
|
--section-content-sz: 1rem;
|
||||||
|
|
||||||
|
// --preview-title-sz: #{v.$text-size};
|
||||||
|
// --preview-subtitle-sz: #{v.$text-size-smaller};
|
||||||
|
// --preview-wide-content-sz: #{v.$text-size};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: v.$screen-wide) {
|
||||||
|
:root {
|
||||||
|
--cover-w: 8rem;
|
||||||
|
--cover-h: 8rem;
|
||||||
|
--cover-small-w: 4rem;
|
||||||
|
--cover-small-h: 4rem;
|
||||||
|
--cover-tiny-w: 2rem;
|
||||||
|
--cover-tiny-h: 2rem;
|
||||||
|
|
||||||
|
--section-content-sz: 1rem;
|
||||||
|
|
||||||
|
// --preview-title-sz: #{v.$text-size};
|
||||||
|
// --preview-subtitle-sz: #{v.$text-size-smaller};
|
||||||
|
// --preview-wide-content-sz: #{v.$text-size};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- headings
|
||||||
|
|
||||||
|
.no-reset h1 { font-size: var(--title-1-sz); }
|
||||||
|
.no-reset h2 { font-size: var(--title-2-sz); }
|
||||||
|
.no-reset h3 { font-size: var(--title-3-sz); }
|
||||||
|
.no-reset h3 { font-size: var(--title-3-sz); }
|
||||||
|
.no-reset h4 { font-size: var(--title-4-sz); }
|
||||||
|
.no-reset h5 { font-size: var(--title-5-sz); }
|
||||||
|
|
||||||
|
.title, .header.preview .title {
|
||||||
|
&.is-1 { font-size: var(--title-1-sz); }
|
||||||
|
&.is-2 { font-size: var(--title-2-sz); }
|
||||||
|
&.is-3 { font-size: var(--title-3-sz); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle, .header.preview .subtitle {
|
||||||
|
color: var(--text-color-light);
|
||||||
|
|
||||||
|
&.is-1 { font-size: var(--subtitle-1-sz); }
|
||||||
|
&.is-2 { font-size: var(--subtitle-2-sz); }
|
||||||
|
&.is-3 { font-size: var(--subtitle-3-sz); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.title + .subtitle {
|
||||||
|
padding-top: 0em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headings a, a.heading, a.subtitle {
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&:not(:empty) {
|
||||||
|
// border-bottom: 1px var(--heading-bg) solid;
|
||||||
|
// color: var(--heading-fg);
|
||||||
|
//padding: v.$mp-2;
|
||||||
|
margin-top: 0em !important;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
&.highlight, &.active,
|
||||||
|
.preview.active &,
|
||||||
|
{
|
||||||
|
// border-color: var(--heading-hg-bg);
|
||||||
|
color: var(--heading-hg-fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- bulma overrides
|
||||||
|
.modal-card {
|
||||||
|
max-width: v.$screen-wide;
|
||||||
|
}
|
||||||
|
.modal-card {
|
||||||
|
max-height: calc(100% - 10rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- button
|
||||||
|
@mixin button {
|
||||||
|
.button, a.button, button.button {
|
||||||
|
font-size: v.$text-size;
|
||||||
|
display: inline-block;
|
||||||
|
padding: v.$mp-2e;
|
||||||
|
border: none;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
color: var(--button-fg);
|
||||||
|
background-color: var(--button-bg);
|
||||||
|
|
||||||
|
&.square { min-width: 2.5em; }
|
||||||
|
&.secondary { background-color: var(--button-sec-bg); }
|
||||||
|
|
||||||
|
.label, label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
vertical-align: middle;
|
||||||
|
&:not(:only-child) {
|
||||||
|
&:first-child { margin: 0 v.$mp-3e 0 v.$mp-1e; }
|
||||||
|
&:last-child { margin: 0 v.$mp-3e 0 v.$mp-1e; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--button-hv-fg);
|
||||||
|
background-color: var(--button-hv-bg);
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active:not(:hover) {
|
||||||
|
color: var(--button-active-fg);
|
||||||
|
background-color: var(--button-active-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not([disabled]), &:not(.disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled], &.disabled {
|
||||||
|
background-color: var(--text-color-light);
|
||||||
|
color: var(--secondary-color);
|
||||||
|
border-color: var(--secondary-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger {
|
||||||
|
border-radius: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.button-group, .nav {
|
||||||
|
.button {
|
||||||
|
border-radius: 0px;
|
||||||
|
background-color: transparent;
|
||||||
|
border-top: 0px;
|
||||||
|
border-bottom: 0px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&:not(:first-child) { border-left: 0px; }
|
||||||
|
&:last-child { border-right: 0px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group + .button-group {
|
||||||
|
border-left: 1px solid var(--text-color-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- preview
|
||||||
|
.preview {
|
||||||
|
position: relative;
|
||||||
|
background-size: cover;
|
||||||
|
background-color: var(--preview-bg) !important;
|
||||||
|
|
||||||
|
&.preview-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: remove
|
||||||
|
&.columns, .headings.columns {
|
||||||
|
margin-left: 0em;
|
||||||
|
margin-right: 0em;
|
||||||
|
.column { padding: 0em; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.title, .title:not(:last-child) {
|
||||||
|
// second is bulma reset
|
||||||
|
font-weight: v.$weight-bold;
|
||||||
|
font-size: var(--preview-title-sz);
|
||||||
|
margin-bottom: unset;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-weight: v.$weight-bolder;
|
||||||
|
font-size: var(--preview-subtitle-sz);
|
||||||
|
margin-bottom: unset;
|
||||||
|
}
|
||||||
|
//.content, .actions {
|
||||||
|
// font-size: v.$text-size-bigger;
|
||||||
|
//}
|
||||||
|
|
||||||
|
.headings {
|
||||||
|
background-size: cover;
|
||||||
|
|
||||||
|
> * { margin: 0em; }
|
||||||
|
.column { padding: 0em; }
|
||||||
|
|
||||||
|
a { color: var(--text-color); }
|
||||||
|
a:hover { color: var(--heading-link-hv-fg) !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tiny {
|
||||||
|
.title { font-size: calc(var(--preview-title-sz) * 0.8); }
|
||||||
|
.subtitle { font-size: calc(var(--preview-subtitle-sz) * 0.8); }
|
||||||
|
.content {
|
||||||
|
font-size: v.$text-size;
|
||||||
|
max-height: 3rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.preview-cover {
|
||||||
|
background: var(--preview-bg);
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
height: var(--cover-h);
|
||||||
|
max-width: calc( var(--cover-w) * 1.5 );
|
||||||
|
min-width: var(--cover-w);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px #c4c4c4 solid;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: var(--cover-h);
|
||||||
|
max-width: calc( var(--cover-w) * 1.5 );
|
||||||
|
min-width: var(--cover-w);
|
||||||
|
}
|
||||||
|
img.hide { visibility: hidden; }
|
||||||
|
|
||||||
|
|
||||||
|
&.small, .preview.small & {
|
||||||
|
min-width: unset;
|
||||||
|
height: var(--cover-small-h);
|
||||||
|
width: var(--cover-small-w) !important;
|
||||||
|
min-width: var(--cover-small-w);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tiny, .preview.tiny & {
|
||||||
|
min-width: unset;
|
||||||
|
height: var(--cover-tiny-h);
|
||||||
|
width: var(--cover-tiny-w) !important;
|
||||||
|
min-width: var(--cover-tiny-w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
// width: 100%;
|
||||||
|
|
||||||
|
/*&:not(.no-cover) {
|
||||||
|
min-height: var(--header-height);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
&.no-cover {
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headings {
|
||||||
|
padding-top: v.$mp-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headings, > .container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .container, {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- list
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
// padding: v.$mp-3;
|
||||||
|
|
||||||
|
.headings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 0em;
|
||||||
|
margin-bottom: v.$mp-2 !important;
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
// background-color: var(--preview-heading-bg-color);
|
||||||
|
padding: 0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.title { flex-grow: 1; }
|
||||||
|
.subtitle {
|
||||||
|
font-size: var(--preview-title-sz);
|
||||||
|
// background-color: var(--preview-heading-bg-color);
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
&:not(:empty) { min-width: 9rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-content {
|
||||||
|
height: 100%;
|
||||||
|
margin-bottom: unset;
|
||||||
|
|
||||||
|
.list-item:not(.no-cover) & {
|
||||||
|
min-height: var(--cover-small-h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
text-align: right;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.wide) .media {
|
||||||
|
padding: v.$mp-3;
|
||||||
|
// border-radius: v.$mp-2;
|
||||||
|
border: 1px solid var(--break-color) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: v.$screen-very-small) {
|
||||||
|
.list-item .headings {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
display: inline;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: unset !important;
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- wide
|
||||||
|
.list-item.wide {
|
||||||
|
& .preview-cover {
|
||||||
|
box-shadow: 0em 0em 1em rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .content {
|
||||||
|
font-size: var(--preview-wide-content-sz);
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- card
|
||||||
|
.preview-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: var(--card-w);
|
||||||
|
padding: 0rem !important;
|
||||||
|
margin-bottom: auto;
|
||||||
|
|
||||||
|
background-color: var(--preview-bg) !important;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
|
||||||
|
&: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);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--heading-link-hv-fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.headings {
|
||||||
|
margin-top: v.$mp-2;
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: v.$text-size-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
figure {
|
||||||
|
// box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2);
|
||||||
|
height: var(--cover-h);
|
||||||
|
width: var(--cover-w);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
position: absolute;
|
||||||
|
padding: v.$mp-2;
|
||||||
|
bottom: 0rem;
|
||||||
|
right: 0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- ---- Carousel
|
||||||
|
.a-carousel {
|
||||||
|
.a-carousel-viewport {
|
||||||
|
box-shadow: inset 0em 0em 20rem var(--a-carousel-bg);
|
||||||
|
// background-color: var(--a-carousel-bg);
|
||||||
|
padding: 0rem;
|
||||||
|
padding-top: var(--a-carousel-p);
|
||||||
|
margin-top: calc( 0rem - var(--a-carousel-p) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-carousel-container {
|
||||||
|
width: 100%;
|
||||||
|
gap: var(--a-carousel-gap);
|
||||||
|
transition: margin-left 1s;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-carousel-bullets-container {
|
||||||
|
// due to a-carousel margin-left
|
||||||
|
padding-left: var(--a-carousel-ml);
|
||||||
|
|
||||||
|
.bullet {
|
||||||
|
margin: v.$mp-1;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover { color: var(--link-fg); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- ---- progress bar
|
||||||
|
.a-progress {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 0em;
|
||||||
|
padding: 0em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--a-progress-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-progress-bar-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
> time, .a-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
padding: var(--a-progress-bar-pd);
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-progress-bar {
|
||||||
|
background-color: var(--a-progress-bar-bg);
|
||||||
|
color: var(--a-progress-bar-color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- ---- player
|
||||||
|
// ---- playlist
|
||||||
|
.playlist, .a-playlist {
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.title, .button {
|
||||||
|
background-color: var(--a-playlist-header-bg);
|
||||||
|
color: var(--a-playlist-header-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--a-playlist-title-sz);
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--a-playlist-title-pd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
border-bottom: var(--a-playlist-item-border);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- sound item
|
||||||
|
.a-sound-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
height: 3rem;
|
||||||
|
background-color: var(--a-sound-bg);
|
||||||
|
|
||||||
|
&.playing .label {
|
||||||
|
color: var(--a-sound-playing-fg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--a-sound-hv-bg);
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: var(--a-sound-hv-fg) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label:hover::before, &.playing .label::before {
|
||||||
|
content: "\f04b";
|
||||||
|
font-family: "Font Awesome 6 Free";
|
||||||
|
margin-right: v.$mp-3e;
|
||||||
|
}
|
||||||
|
&.playing .label:hover::before {
|
||||||
|
content: '';
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.headings > * {
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
padding: 0em v.$mp-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
margin: 0em !important;
|
||||||
|
padding: v.$mp-3e;
|
||||||
|
font-size: var(--a-sound-text-sz);
|
||||||
|
font-family: var(--heading-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 3em;
|
||||||
|
font-size: var(--a-sound-text-sz);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--a-sound-hv-fg) !important;
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- player
|
||||||
|
.player-container {
|
||||||
|
z-index: 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-player {
|
||||||
|
box-shadow: 0em -0.5em 0.5em rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
a { color: var(--a-player-url-fg); }
|
||||||
|
.button {
|
||||||
|
color: var(--text-black);
|
||||||
|
&:hover { color: var(--button-fg); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-player-panels {
|
||||||
|
background: var(--a-player-panel-bg);
|
||||||
|
height: 0%;
|
||||||
|
transition: height 1s;
|
||||||
|
}
|
||||||
|
.a-player-panels.is-open {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-player-panel {
|
||||||
|
padding-bottom: v.$mp-3;
|
||||||
|
max-height: 80%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.a-sound-item:not(:hover) {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-player-progress {
|
||||||
|
height: 0.4em;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
time { display: none; }
|
||||||
|
|
||||||
|
&:hover, .a-player-panels.is-open + & {
|
||||||
|
background: var(--a-player-bar-bg);
|
||||||
|
height: 2em;
|
||||||
|
time { display: unset; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-player-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
height: var(--a-player-bar-height);
|
||||||
|
|
||||||
|
border-top: 1px v.$grey-light solid;
|
||||||
|
background: var(--a-player-bar-bg);
|
||||||
|
|
||||||
|
> * { height: 100%; }
|
||||||
|
|
||||||
|
.cover { height: 100%; }
|
||||||
|
.title {
|
||||||
|
font-size: v.$text-size;
|
||||||
|
margin: 0em;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
font-size: var(--a-player-bar-title-alone-sz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
font-size: v.$text-size-medium;
|
||||||
|
height: 100%;
|
||||||
|
padding: v.$mp-2 !important;
|
||||||
|
min-width: calc(var(--a-player-bar-height) + v.$mp-2 * 2);
|
||||||
|
border-radius: 0px;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
background-color: var(--button-active-bg);
|
||||||
|
color: var(--button-active-fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-player-bar-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: vertical;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 0 v.$mp-3;
|
||||||
|
border-right: 1px black solid;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
max-height: calc( var(--a-player-bar-height) - v.$mp-3 );
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// ---- playlist editor
|
||||||
|
.a-tracklist-editor {
|
||||||
|
.dropdown {
|
||||||
|
display: unset !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ----------------
|
||||||
|
.a-select-file {
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-bottom: v.$mp-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-preview {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-select-file-list {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
165
radiocampus/assets/src/styles/helpers.scss
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
@use "./vars" as v;
|
||||||
|
|
||||||
|
// ---- text
|
||||||
|
.text-light { font-weight: 400; color: var(--text-color-light); }
|
||||||
|
|
||||||
|
.bigger { font-size: v.$text-size-bigger !important; }
|
||||||
|
.big { font-size: v.$text-size-big !important; }
|
||||||
|
.smaller { font-size: v.$text-size-smaller !important; }
|
||||||
|
.small { font-size: v.$text-size-small !important; }
|
||||||
|
|
||||||
|
// ---- layout
|
||||||
|
.align-left {
|
||||||
|
text-align: left;
|
||||||
|
justify-content: left;
|
||||||
|
|
||||||
|
&.x { padding-left: 0px !important; }
|
||||||
|
}
|
||||||
|
.align-right {
|
||||||
|
text-align: right;
|
||||||
|
justify-content: right;
|
||||||
|
|
||||||
|
&.x { padding-right: 0px !important; }
|
||||||
|
}
|
||||||
|
.align-center {
|
||||||
|
text-align: center !important;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
|
||||||
|
.height-1 { height: 1em; }
|
||||||
|
.height-2 { height: 2em; }
|
||||||
|
.height-3 { height: 3em; }
|
||||||
|
.height-4 { height: 4em; }
|
||||||
|
.height-5 { height: 5em; }
|
||||||
|
.height-6 { height: 6em; }
|
||||||
|
.height-7 { height: 7em; }
|
||||||
|
.height-8 { height: 8em; }
|
||||||
|
.height-9 { height: 9em; }
|
||||||
|
.height-10 { height: 10em; }
|
||||||
|
.height-15 { height: 15em; }
|
||||||
|
.height-20 { height: 20em; }
|
||||||
|
.height-25 { height: 25em; }
|
||||||
|
|
||||||
|
// ---- grid / flex
|
||||||
|
|
||||||
|
.gap-1 { gap: v.$mp-1 !important; }
|
||||||
|
.gap-2 { gap: v.$mp-2 !important; }
|
||||||
|
.gap-3 { gap: v.$mp-3 !important; }
|
||||||
|
.gap-4 { gap: v.$mp-4 !important; }
|
||||||
|
.gap-5 { gap: v.$mp-5 !important; }
|
||||||
|
|
||||||
|
|
||||||
|
// ---- ---- 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; }
|
||||||
|
.flex-grow-1 { flex-grow: 1 !important; }
|
||||||
|
.flex-grow-2 { flex-grow: 2 !important; }
|
||||||
|
.flex-grow-3 { flex-grow: 3 !important; }
|
||||||
|
.flex-grow-4 { flex-grow: 4 !important; }
|
||||||
|
.flex-grow-5 { flex-grow: 5 !important; }
|
||||||
|
.flex-grow-6 { flex-grow: 6 !important; }
|
||||||
|
|
||||||
|
.float-right { float: right }
|
||||||
|
.float-left { float: left }
|
||||||
|
|
||||||
|
// ---- boxing
|
||||||
|
.is-fullwidth { width: 100%; }
|
||||||
|
.is-fullheight { height: 100%; }
|
||||||
|
.is-fixed-bottom {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.no-border { border: 0px !important; }
|
||||||
|
|
||||||
|
.overflow-hidden { overflow: hidden }
|
||||||
|
.overflow-hidden.is-fullwidth { max-width: 100%; }
|
||||||
|
|
||||||
|
.height-full { height: 100%; }
|
||||||
|
|
||||||
|
*[draggable="true"] {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- animations
|
||||||
|
@keyframes blink {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0.4; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.blink { animation: 1s ease-in-out 3s infinite alternate blink; }
|
||||||
|
.loading { animation: 1s ease-in-out 1s infinite alternate blink; }
|
||||||
|
|
||||||
|
|
||||||
|
// -- colors
|
||||||
|
.main-color { color: var(--main-color); }
|
||||||
|
.secondary-color { color: var(--secondary-color); }
|
||||||
|
|
||||||
|
.bg-main { background-color: var(--main-color); }
|
||||||
|
.bg-main-light { background-color: var(--main-color-light); }
|
||||||
|
.bg-secondary { background-color: var(--secondary-color); }
|
||||||
|
.bg-secondary-light { background-color: var(--secondary-color-light); }
|
||||||
|
.bg-transparent { background-color: transparent; }
|
||||||
|
|
||||||
|
.border { border: 1px solid var(--text-color); }
|
||||||
|
.border-main { border: 1px solid var(--main-color); }
|
||||||
|
.border-secondary { border: 1px solid var(--secondary-color); }
|
||||||
|
.border-bottom-main { border-bottom: 1px solid var(--main-color); }
|
||||||
|
.border-bottom-secondary { border-bottom: 1px solid var(--secondary-color); }
|
||||||
|
|
||||||
|
.is-success {
|
||||||
|
background-color: v.$green !important;
|
||||||
|
border-color: v.$green-dark !important;
|
||||||
|
}
|
||||||
|
.is-danger {
|
||||||
|
background-color: v.$red !important;
|
||||||
|
border-color: v.$red-dark !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.box-shadow {
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0em 0em 1em rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
box-shadow: 0em 0em 1em rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
}
|
478
radiocampus/assets/src/styles/public.scss
Normal file
@ -0,0 +1,478 @@
|
|||||||
|
@use "./vars" as v;
|
||||||
|
@use "./components";
|
||||||
|
|
||||||
|
|
||||||
|
// ---- main theme & layout
|
||||||
|
|
||||||
|
.page {
|
||||||
|
padding-bottom: 5rem;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--link-fg);
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--link-hv-fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.container {
|
||||||
|
margin-top: v.$mp-3;
|
||||||
|
margin-bottom: v.$mp-4;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
padding-bottom: calc(v.$mp-4 / 2);
|
||||||
|
// border-bottom: 2px var(--break-color) solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .title, h3.title {
|
||||||
|
font-size: var(--title-2-sz);
|
||||||
|
clear: both;
|
||||||
|
margin: v.$mp-3 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
*[data-oembed-url] {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ---- components
|
||||||
|
.dropdown-item {
|
||||||
|
font-size: unset !important
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-weekday-1, .vc-weekday-7 {
|
||||||
|
color: var(--secondary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.schedules {
|
||||||
|
padding-top: 0;
|
||||||
|
margin-bottom: calc(0rem - v.$mp-3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule {
|
||||||
|
display: inline-block;
|
||||||
|
margin: v.$mp-3;
|
||||||
|
margin-left: 0rem;
|
||||||
|
padding: v.$mp-2;
|
||||||
|
text-color: var(--main-color);
|
||||||
|
background-color: var(--main-color-light);
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
padding: 0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day {
|
||||||
|
font-weight: v.$weight-bold;
|
||||||
|
margin-right: v.$mp-3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -- buttons, forms
|
||||||
|
@include components.button;
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: v.$mp-3;
|
||||||
|
justify-content: right;
|
||||||
|
|
||||||
|
&.no-label label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, .action, a {
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 2rem;
|
||||||
|
padding: v.$mp-2;
|
||||||
|
|
||||||
|
.not-selected { opacity: 0.6; }
|
||||||
|
.icon { margin: 0em !important; }
|
||||||
|
label { margin-left: v.$mp-2; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.label, .textarea, .input, .select {
|
||||||
|
font-size: v.$text-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field.is-horizontal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: horizontal;
|
||||||
|
|
||||||
|
.label { min-width: 7rem }
|
||||||
|
.control {
|
||||||
|
flex: 1;
|
||||||
|
> * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: v.$screen-small) {
|
||||||
|
comment.textarea {
|
||||||
|
height: calc( v.$text-size * 7 ) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-item.active, .table tr.is-selected {
|
||||||
|
color: var(--secondary-color);
|
||||||
|
background-color: var(--main-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -- headings
|
||||||
|
.title {
|
||||||
|
text-transform: uppercase;
|
||||||
|
&.is-3 { margin-top: v.$mp-3; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- main navigation
|
||||||
|
.navs {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
background-color: var(--nav-bg);
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger {
|
||||||
|
display: none;
|
||||||
|
background-color: var(--nav-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: v.$mp-2;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
font-family: var(--heading-font-family);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--nav-fg) !important;
|
||||||
|
|
||||||
|
.icon:first-child, .icon + span {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: top;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--nav-hv-bg);
|
||||||
|
color: var(--nav-hv-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: var(--nav-active-bg);
|
||||||
|
color: var(--nav-active-fg) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
font-size: v.$text-size;
|
||||||
|
min-width: 15rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
height: var(--nav-primary-height);
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand {
|
||||||
|
display: inline-block;
|
||||||
|
padding: v.$mp-3;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
font-size: var(--nav-fs);
|
||||||
|
font-weight: v.$weight-bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
background-color: var(--nav-secondary-bg);
|
||||||
|
//position: absolute;
|
||||||
|
//width: 100%;
|
||||||
|
//box-shadow: 0em 0.5em 0.5em rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
justify-content: right;
|
||||||
|
//display: none;
|
||||||
|
|
||||||
|
.nav.primary:hover + &,
|
||||||
|
&:hover {
|
||||||
|
display: flex;
|
||||||
|
top: var(--nav-primary-height);
|
||||||
|
left: 0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
font-size: var(--nav-2-fs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- breadcrumbs
|
||||||
|
.breadcrumbs {
|
||||||
|
text-align: right;
|
||||||
|
padding: v.$mp-3 0rem;
|
||||||
|
font-size: v.$text-size-smaller;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
&:empty { display: none; }
|
||||||
|
|
||||||
|
a + a {
|
||||||
|
padding-left: 0;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "/";
|
||||||
|
margin: 0 v.$mp-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (max-width: v.$screen-normal) {
|
||||||
|
.page {
|
||||||
|
margin-top: var(--nav-primary-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navs {
|
||||||
|
z-index: 100000;
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
.nav:first-child {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav + .nav {
|
||||||
|
flex-grow: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.burger {
|
||||||
|
display: unset;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--nav-secondary-bg);
|
||||||
|
left: 0;
|
||||||
|
top: 100%;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0em 0.5em 0.5em rgba(0,0,0,0.05);
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: block;
|
||||||
|
font-weight: v.$weight-normal;
|
||||||
|
font-size: var(--nav-fs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu:not(.active) {
|
||||||
|
display: none !important
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
nav li {
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
a, .button {
|
||||||
|
font-size: v.$text-size-medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.nav-urls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
margin-top: v.$mp-3;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
> a:only-child {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.urls {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: v.$mp-3;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
a:not(:last-child) {
|
||||||
|
margin-right: v.$mp-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
flex-grow: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
flex-grow: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- page header
|
||||||
|
.header {
|
||||||
|
&.preview-header {
|
||||||
|
//display: flex;
|
||||||
|
align-items: start;
|
||||||
|
gap: v.$mp-3;
|
||||||
|
min-height: unset;
|
||||||
|
padding-top: v.$mp-3 !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.headings {
|
||||||
|
width: unset;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-cover {
|
||||||
|
min-height: calc( var(--header-height) / 3 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.header-cover:not(:only-child) {
|
||||||
|
float: right;
|
||||||
|
position: relative;
|
||||||
|
z-index: 30;
|
||||||
|
background-color: var(--body-bg);
|
||||||
|
margin: 0 0 v.$mp-4 v.$mp-4;
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
max-width: calc(var(--header-height) * 2);
|
||||||
|
height: var(--header-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.header-cover:only-child {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: v.$screen-small) {
|
||||||
|
.container.header {
|
||||||
|
width: calc( 100% - v.$mp-2 );
|
||||||
|
|
||||||
|
.headings {
|
||||||
|
width: 100%;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-cover {
|
||||||
|
float: none;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-height: calc(var(--cover-h) * 1);
|
||||||
|
max-width: calc(var(--cover-w) * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- ---- detail
|
||||||
|
.page-content {
|
||||||
|
margin-top: v.$mp-6;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: v.$mp-6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- ---- list
|
||||||
|
.list-item {
|
||||||
|
&.logs {
|
||||||
|
.track {
|
||||||
|
margin-right: v.$mp-3;
|
||||||
|
.icon {
|
||||||
|
margin-right: v.$mp-2;
|
||||||
|
color: var(--secondary-color-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3n):not(.wide) .media,
|
||||||
|
{
|
||||||
|
border-color: var(--main-color-dark) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3n+1):not(.wide) .media,
|
||||||
|
{
|
||||||
|
border-color: var(--secondary-color-dark) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ---- responsive
|
||||||
|
@media screen and (max-width: v.$screen-normal) {
|
||||||
|
.page .container {
|
||||||
|
margin-left: v.$mp-4;
|
||||||
|
margin-right: v.$mp-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: v.$screen-small) {
|
||||||
|
.page .container {
|
||||||
|
margin-left: v.$mp-2;
|
||||||
|
margin-right: v.$mp-2;
|
||||||
|
}
|
||||||
|
}
|
52
radiocampus/assets/src/styles/vars.scss
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
@charset "utf-8";
|
||||||
|
|
||||||
|
$black: #000;
|
||||||
|
$white: #fff;
|
||||||
|
$red: #e00;
|
||||||
|
$red-dark: #b00;
|
||||||
|
$green: #0e0;
|
||||||
|
$green-dark: #0b0;
|
||||||
|
$grey-light: #ddd;
|
||||||
|
|
||||||
|
$mp-1: 0.2rem;
|
||||||
|
$mp-1e: 0.2em;
|
||||||
|
$mp-2: 0.4rem;
|
||||||
|
$mp-2e: 0.4em;
|
||||||
|
$mp-3: 0.6rem;
|
||||||
|
$mp-3e: 0.6em;
|
||||||
|
$mp-4: 1.2rem;
|
||||||
|
$mp-4e: 1.2em;
|
||||||
|
$mp-5: 1.6rem;
|
||||||
|
$mp-5e: 1.6em;
|
||||||
|
$mp-6: 2rem;
|
||||||
|
$mp-6e: 2em;
|
||||||
|
$mp-7: 4rem;
|
||||||
|
$mp-7e: 4em;
|
||||||
|
|
||||||
|
$text-size-small: 0.6rem;
|
||||||
|
$text-size-smaller: 0.8rem;
|
||||||
|
$text-size: 1rem;
|
||||||
|
$text-size-2: 1.2rem;
|
||||||
|
$text-size-medium: 1.4rem;
|
||||||
|
$text-size-bigger: 1.6rem;
|
||||||
|
$text-size-big: 2rem;
|
||||||
|
|
||||||
|
$h1-size: 40px;
|
||||||
|
$h2-size: 32px;
|
||||||
|
$h3-size: 28px;
|
||||||
|
$h4-size: 24px;
|
||||||
|
$h5-size: 20px;
|
||||||
|
$h6-size: 14px;
|
||||||
|
|
||||||
|
$weight-light: 100;
|
||||||
|
$weight-lighter: 300;
|
||||||
|
$weight-normal: 400;
|
||||||
|
$weight-bolder: 500;
|
||||||
|
$weight-bold: 700;
|
||||||
|
|
||||||
|
$screen-very-small: 400px;
|
||||||
|
$screen-small: 600px;
|
||||||
|
$screen-smaller: 900px;
|
||||||
|
$screen-normal: 1024px;
|
||||||
|
$screen-wider: 1280px;
|
||||||
|
$screen-wide: 1380px;
|
35
radiocampus/assets/src/styles/vendor.scss
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
@import 'v-calendar/style.css';
|
||||||
|
// @import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||||
|
|
||||||
|
// ---- bulma
|
||||||
|
$body-color: #000;
|
||||||
|
$title-color: #000;
|
||||||
|
$modal-content-width: 80%;
|
||||||
|
|
||||||
|
|
||||||
|
@import "bulma/sass/utilities/_all.sass";
|
||||||
|
|
||||||
|
|
||||||
|
@import "bulma/sass/base/_all";
|
||||||
|
@import "bulma/sass/components/dropdown";
|
||||||
|
// @import "bulma/sass/components/card";
|
||||||
|
@import "bulma/sass/components/media";
|
||||||
|
@import "bulma/sass/components/message";
|
||||||
|
@import "bulma/sass/components/modal";
|
||||||
|
//@import "bulma/sass/components/pagination";
|
||||||
|
|
||||||
|
@import "bulma/sass/form/_all";
|
||||||
|
@import "bulma/sass/grid/_all";
|
||||||
|
@import "bulma/sass/helpers/_all";
|
||||||
|
@import "bulma/sass/layout/_all";
|
||||||
|
@import "bulma/sass/elements/box";
|
||||||
|
// @import "bulma/sass/elements/button";
|
||||||
|
@import "bulma/sass/elements/container";
|
||||||
|
// @import "bulma/sass/elements/content";
|
||||||
|
@import "bulma/sass/elements/icon";
|
||||||
|
// @import "bulma/sass/elements/image";
|
||||||
|
// @import "bulma/sass/elements/notification";
|
||||||
|
// @import "bulma/sass/elements/progress";
|
||||||
|
@import "bulma/sass/elements/table";
|
||||||
|
@import "bulma/sass/elements/tag";
|
||||||
|
//@import "bulma/sass/elements/title";
|
17
radiocampus/assets/src/utils.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Run function with provided args only if document is not hidden
|
||||||
|
*/
|
||||||
|
export function setEcoTimeout(func, ...args) {
|
||||||
|
return setTimeout((...args) => {
|
||||||
|
!document.hidden && func(...args)
|
||||||
|
}, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run function at specific interval only if document is not hidden
|
||||||
|
*/
|
||||||
|
export function setEcoInterval(func, ...args) {
|
||||||
|
return setInterval((...args) => {
|
||||||
|
!document.hidden && func(...args)
|
||||||
|
}, ...args)
|
||||||
|
}
|
50
radiocampus/assets/src/vueLoader.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import {createApp} from 'vue'
|
||||||
|
|
||||||
|
import PageLoad from './pageLoad'
|
||||||
|
import BackgroundLoad from './backgroundLoad'
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles loading Vue js app on page load.
|
||||||
|
*/
|
||||||
|
export default class VueLoader {
|
||||||
|
constructor({el=null, props={}, ...appConfig}={}, loaderOptions={}) {
|
||||||
|
this.appConfig = appConfig
|
||||||
|
this.appConfig.el = el
|
||||||
|
this.props = props
|
||||||
|
this.pageLoad = new PageLoad(el, loaderOptions)
|
||||||
|
|
||||||
|
this.pageLoad.onPreMount = event => this.onPreMount(event)
|
||||||
|
this.pageLoad.onMount = event => this.onMount(event)
|
||||||
|
|
||||||
|
this.backgroundLoad = new BackgroundLoad()
|
||||||
|
}
|
||||||
|
|
||||||
|
enable(hotReload=true) {
|
||||||
|
hotReload && this.pageLoad.enable(document.body)
|
||||||
|
this.mount()
|
||||||
|
}
|
||||||
|
|
||||||
|
mount() {
|
||||||
|
if(this.app)
|
||||||
|
this.unmount()
|
||||||
|
|
||||||
|
const app = createApp(this.appConfig, this.props)
|
||||||
|
app.config.globalProperties.window = window
|
||||||
|
this.vm = app.mount(this.pageLoad.el)
|
||||||
|
this.app = app
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount() {
|
||||||
|
if(!this.app)
|
||||||
|
return
|
||||||
|
try { this.app.unmount() }
|
||||||
|
catch(_) { null }
|
||||||
|
this.app = null
|
||||||
|
this.vm = null
|
||||||
|
this.pageLoad.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
onPreMount() { this.unmount() }
|
||||||
|
onMount() { this.mount() }
|
||||||
|
}
|
44
radiocampus/assets/vite.config.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { resolve } from 'path'
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
outDir: "../static/aircox/",
|
||||||
|
sourcemap: true,
|
||||||
|
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['vue',],
|
||||||
|
input: {
|
||||||
|
public: "src/public.js",
|
||||||
|
admin: "src/admin.js",
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
vue: 'Vue',
|
||||||
|
},
|
||||||
|
assetFileNames: "[name].[ext]",
|
||||||
|
chunkFileNames: "[name].js",
|
||||||
|
entryFileNames: "[name].js",
|
||||||
|
},
|
||||||
|
plugins: [commonjs()],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
devSourcemap: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.ts', '.json', '.vue'],
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
48
radiocampus/static/radiocampus/backgroundLoad.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Enable styling body background while using vue hotreload
|
||||||
|
// Tags with side effect (<script> and <style>) are ignored in client component templates.
|
||||||
|
|
||||||
|
|
||||||
|
class BackgroundLoad {
|
||||||
|
// change background style on load
|
||||||
|
// and also on vuejs pageLoaded event
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
let url = new URL(document.location);
|
||||||
|
this.path = url.pathname;
|
||||||
|
this.update();
|
||||||
|
document.addEventListener("pageLoaded", this.handlePageLoad.bind(this), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePageLoad (e) {
|
||||||
|
this.path = e.detail;
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
update () {
|
||||||
|
let theme = this.get_theme_name();
|
||||||
|
document.body.className = theme;
|
||||||
|
|
||||||
|
// home page uses different theme
|
||||||
|
if (this.path == "/") {
|
||||||
|
document.body.classList.add('home');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
get_theme_name () {
|
||||||
|
var currentTime = new Date().getHours();
|
||||||
|
if (document.body) {
|
||||||
|
if (3 <= currentTime && currentTime <15) {
|
||||||
|
return "yellow";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "blue";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
let backgroundLoad = new BackgroundLoad();
|
||||||
|
}
|
BIN
radiocampus/static/radiocampus/backgrounds/degrade-01.jpg
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
radiocampus/static/radiocampus/backgrounds/degrade-02.jpg
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
radiocampus/static/radiocampus/backgrounds/degrade-bleu.jpg
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
radiocampus/static/radiocampus/backgrounds/degrade-jaune.jpg
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
radiocampus/static/radiocampus/backgrounds/photo-degrade-01.jpg
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
radiocampus/static/radiocampus/backgrounds/photo-degrade-02.jpg
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
radiocampus/static/radiocampus/images/grand-logo.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
radiocampus/static/radiocampus/images/logo-small-white.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
radiocampus/static/radiocampus/images/logo-small.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
radiocampus/static/radiocampus/logos/Noir/logo-RC-final-01.png
Normal file
After Width: | Height: | Size: 162 KiB |
BIN
radiocampus/static/radiocampus/logos/Noir/logo-RC-final-02.png
Normal file
After Width: | Height: | Size: 156 KiB |
BIN
radiocampus/static/radiocampus/logos/Noir/logo-RC-final-04.png
Normal file
After Width: | Height: | Size: 184 KiB |
BIN
radiocampus/static/radiocampus/logos/Noir/logo-RC-final-05.png
Normal file
After Width: | Height: | Size: 178 KiB |
BIN
radiocampus/static/radiocampus/logos/Noir/logo-RC-final-06.png
Normal file
After Width: | Height: | Size: 106 KiB |
BIN
radiocampus/static/radiocampus/logos/Noir/logo-RC-final-07.png
Normal file
After Width: | Height: | Size: 122 KiB |
BIN
radiocampus/static/radiocampus/logos/Noir/logo-RC-final-08.png
Normal file
After Width: | Height: | Size: 124 KiB |
BIN
radiocampus/static/radiocampus/logos/blanc/logo-RC-final-01.png
Normal file
After Width: | Height: | Size: 154 KiB |
BIN
radiocampus/static/radiocampus/logos/blanc/logo-RC-final-02.png
Normal file
After Width: | Height: | Size: 147 KiB |
BIN
radiocampus/static/radiocampus/logos/blanc/logo-RC-final-03.png
Normal file
After Width: | Height: | Size: 107 KiB |
BIN
radiocampus/static/radiocampus/logos/blanc/logo-RC-final-04.png
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
radiocampus/static/radiocampus/logos/blanc/logo-RC-final-05.png
Normal file
After Width: | Height: | Size: 168 KiB |
BIN
radiocampus/static/radiocampus/logos/blanc/logo-RC-final-06.png
Normal file
After Width: | Height: | Size: 103 KiB |
BIN
radiocampus/static/radiocampus/logos/blanc/logo-RC-final-07.png
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
radiocampus/static/radiocampus/logos/blanc/logo-RC-final-08.png
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
radiocampus/static/radiocampus/logos/logo-RC-blanc1.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
radiocampus/static/radiocampus/logos/logo-RC-blanc2.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
radiocampus/static/radiocampus/logos/logo-RC-bleu1.png
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
radiocampus/static/radiocampus/logos/logo-RC-bleu2.png
Normal file
After Width: | Height: | Size: 77 KiB |
250
radiocampus/static/radiocampus/logos/logo-RC-final.svg
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1190.6 841.9">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-1, .cls-2 {
|
||||||
|
stroke-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M215.8-748.5c24.6.2,40.9,16,40.9,39.6s-15.6,43-38.9,43h-28.5l59,62.9h-25.2l-56.9-62.9h-12.6v62.9h-18.8v-145.5h81.1ZM153.5-684.1h64c13.3-.2,21.6-9.6,21.6-23.8s-8.3-22.3-21.6-22.5h-64v46.2Z"/>
|
||||||
|
<path class="cls-2" d="M370.4-651.2h-66.3l-19.5,48.1h-19.3l58.4-145.5h26.7l58.4,145.5h-19.3l-19.3-48.1ZM363.2-669.4l-22.7-56.5-3.2-8.1-3.2,8.1-22.7,56.5h51.8Z"/>
|
||||||
|
<path class="cls-2" d="M489.9-748.5c36.8.2,60.1,30.2,60.1,70.8s-19,74.7-61.4,74.7h-60.6v-145.5h61.8ZM446.9-621.2h45.6c25.5,0,39.4-22,39.4-55.4s-16.1-53.5-40.2-53.7h-44.7v109.1h0Z"/>
|
||||||
|
<path class="cls-2" d="M657.2-620.8v17.8h-94.1v-17.8h38.5v-110h-32.1v-17.8h81.3v17.8h-31.5v110h37.9Z"/>
|
||||||
|
</g>
|
||||||
|
<path class="cls-2" d="M369.4-476.5h-66.3l-19.5,48.1h-19.3l58.4-145.5h26.7l58.4,145.5h-19.3l-19.3-48.1ZM362.1-494.7l-22.7-56.5-3.2-8.1-3.2,8.1-22.7,56.5h51.8Z"/>
|
||||||
|
<path class="cls-2" d="M678.2-573.6c24.6.2,40.9,16,40.9,39.6s-15.6,43-38.9,43h-59.9v62.9h-18.8v-145.5h76.8ZM620.2-509.2h59.7c13.3-.2,21.6-9.6,21.6-23.7s-8.3-22.2-21.6-22.5h-59.7v46.2Z"/>
|
||||||
|
<path class="cls-2" d="M849.6-471.1c-.2,29.8-20.5,47.3-54.8,47.3s-57.8-18.2-57.8-45.4v-104.4h19v102.1c.2,18.8,14.1,29.1,37.7,29.1s36.6-10.3,36.8-29.1v-102.1h19v102.5h0Z"/>
|
||||||
|
<path class="cls-2" d="M929.4-442c17.8,0,30.8-6.8,30.8-21.4s-10.3-18.8-19.5-22c-11.8-4.1-21.4-5.6-35.1-9.6-17.1-5.1-29.7-19.7-29.7-40s15.6-42.6,47.7-42.6,38.1,4.1,55,22.5l-12.8,13.1c-15.2-14.8-34.2-17.3-43.2-17.3-17.3,0-27.8,11.1-27.8,23.5s6,19.3,20.1,23.3c7.3,2.1,18.6,5.4,34.4,10.1,23.5,7.1,29.7,21.4,29.7,38.7s-19.9,40-49.6,40-43.2-7.7-56.1-25.9l15.6-11.6c10.7,13.9,24.4,19.3,40.4,19.3Z"/>
|
||||||
|
<path class="cls-2" d="M536.2-272.3v18.2h-103.2v-145.9h18.2v127.7h85Z"/>
|
||||||
|
<path class="cls-2" d="M253.4-296.5c0,26.3-19.1,43-46,42.4l-9-.2h-63.4v-145.6h69.8c27.2,1.1,39,18.2,39,38.5s-3.9,21-9.2,27c10.7,7.3,18.8,20.8,18.8,37.9ZM153.2-340.8h51.8c14.8,0,20.5-8.6,20.5-20.3s-5.6-20.6-24.2-20.6h-48.2v40.9ZM235.2-298c0-13.5-7.3-24.6-24.8-24.6h-57.2v50.1h55c19.7,0,27-10.5,27-25.5Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M301.9-381.8c0,10.1-8.2,18.3-18.3,18.3s-18.3-8.2-18.3-18.3,8.2-18.3,18.3-18.3,18.3,8.2,18.3,18.3Z"/>
|
||||||
|
<circle class="cls-2" cx="393.6" cy="-381.8" r="18.3"/>
|
||||||
|
<path class="cls-2" d="M338.6-345.3c0,10.1-8.2,18.3-18.3,18.4-10.1,0-18.3-8.2-18.4-18.3,0-10.1,8.2-18.3,18.3-18.4,10.1,0,18.3,8.2,18.4,18.3Z"/>
|
||||||
|
<circle class="cls-2" cx="356.9" cy="-345.3" r="18.3"/>
|
||||||
|
<path class="cls-2" d="M338.6-308.6c0,10.1-8.2,18.3-18.3,18.4-10.1,0-18.3-8.2-18.4-18.3,0-10.1,8.2-18.3,18.3-18.4,10.1,0,18.3,8.2,18.4,18.3Z"/>
|
||||||
|
<circle class="cls-2" cx="357" cy="-308.7" r="18.3"/>
|
||||||
|
<path class="cls-2" d="M301.9-271.8c0,10.1-8.2,18.3-18.3,18.3s-18.3-8.2-18.3-18.3,8.2-18.3,18.3-18.3c10.1,0,18.3,8.2,18.3,18.3Z"/>
|
||||||
|
<circle class="cls-2" cx="393.6" cy="-271.8" r="18.3"/>
|
||||||
|
</g>
|
||||||
|
<polygon class="cls-2" points="428.4 -573.9 428.4 -428.6 439 -428.6 439 -557.1 480.8 -557.1 480.8 -428.6 501.2 -428.6 501.2 -557.1 544.2 -557.1 544.2 -428.6 569.8 -428.6 569.8 -573.9 428.4 -573.9"/>
|
||||||
|
<polygon class="cls-2" points="253.5 -428.2 172.8 -428.2 172.8 -463.1 134.9 -463.1 134.9 -549.4 173.3 -549.4 173.3 -574.4 216.5 -574.4 216.5 -557.4 190.3 -557.4 190.3 -532.4 151.9 -532.4 151.9 -480.1 189.8 -480.1 189.8 -445.2 253.5 -445.2 253.5 -428.2"/>
|
||||||
|
<path class="cls-2" d="M809.9-693.8c6.1,4.5,12.4,8.6,18.8,12.5v-21.3c-2.8-1.8-5.5-3.7-8.1-5.7-16-11.8-29.8-25.3-41.5-40.3h-22.2c14.1,20.9,31.9,39.2,53,54.8Z"/>
|
||||||
|
<path class="cls-2" d="M695.8-655.8c-7.1-5.4-14.5-10.4-22.1-14.9v21.3c3.8,2.5,7.5,5.2,11.2,7.9,14.8,11.3,27.8,24.1,39,38.4h22.3c-13.7-20-30.6-37.7-50.4-52.8Z"/>
|
||||||
|
<path class="cls-2" d="M828.7-648.7v-21.6c-3.8,2.4-7.5,4.8-11.1,7.4-24.3,17-44.8,37.5-60.5,60h22.5c13.7-17.2,30.4-32.7,49.1-45.7Z"/>
|
||||||
|
<path class="cls-2" d="M746.5-748.7h-22.1c-13.9,18.4-31.1,34.2-50.8,47v21.1c29.4-17.1,54.4-40.3,72.8-68.1Z"/>
|
||||||
|
<path class="cls-2" d="M760-347.3c0,69.6-16.9,97.2-53.1,97.2s-39.4-10.3-47.7-25.7l14.1-10.1c6.6,12.2,17.1,18,34.5,18s32.3-12.4,34.5-44.3c-9,7.9-21.2,13.1-34.7,13.1-31.7,0-52.2-24-52.2-48.2s20.8-56.9,52.2-56.9c31.3,0,52.4,24.2,52.4,56.9ZM743.8-350.5c0-23.1-17.6-34.2-36.2-34.2s-34.7,11.1-34.7,34.2,14.8,33.4,34.7,33.4c20.8,0,36.2-13.5,36.2-33.4Z"/>
|
||||||
|
<path class="cls-2" d="M875.6-272.8v18.4h-92.5v-18.4c57.4-42.6,71.7-60.8,71.7-83.7s-11.3-28.9-31.7-28.9-33,11.3-26.8,39.2l-17.1,7.9c-9.2-30.2,4.7-65.9,45.6-65.9s47.3,18.2,47.3,43.9-21,59.5-62.9,87.6h66.4Z"/>
|
||||||
|
<path class="cls-2" d="M907.7-276.4v22h-22v-22h22Z"/>
|
||||||
|
<path class="cls-2" d="M960.6-399.9h18.4v145.8h-18.4v-122.7l-39,37.9v-22.7l39-38.3Z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M1448.4-1634.5c24.6.2,40.9,16,40.9,39.6s-15.6,43-38.9,43h-28.5l59,62.9h-25.2l-56.9-62.9h-12.6v62.9h-18.8v-145.5h81.1ZM1386.1-1570.1h64c13.3-.2,21.6-9.6,21.6-23.8s-8.3-22.3-21.6-22.5h-64v46.2Z"/>
|
||||||
|
<path class="cls-2" d="M1603-1537.1h-66.3l-19.5,48.1h-19.3l58.4-145.5h26.7l58.4,145.5h-19.3l-19.3-48.1ZM1595.7-1555.3l-22.7-56.5-3.2-8.1-3.2,8.1-22.7,56.5h51.8Z"/>
|
||||||
|
<path class="cls-2" d="M1722.4-1634.5c36.8.2,60.1,30.2,60.1,70.8s-19,74.7-61.4,74.7h-60.6v-145.5h61.8ZM1679.4-1507.2h45.6c25.5,0,39.4-22,39.4-55.4s-16.1-53.5-40.2-53.7h-44.7v109.1h0Z"/>
|
||||||
|
<path class="cls-2" d="M1889.8-1506.7v17.8h-94.1v-17.8h38.5v-110h-32.1v-17.8h81.3v17.8h-31.5v110h37.9Z"/>
|
||||||
|
<path class="cls-2" d="M1602-1362.4h-66.3l-19.5,48.1h-19.3l58.4-145.5h26.7l58.4,145.5h-19.3l-19.3-48.1ZM1594.7-1380.6l-22.7-56.5-3.2-8.1-3.2,8.1-22.7,56.5h51.8Z"/>
|
||||||
|
<path class="cls-2" d="M1910.8-1459.6c24.6.2,40.9,16,40.9,39.6s-15.6,43-38.9,43h-59.9v62.9h-18.8v-145.5h76.8ZM1852.8-1395.2h59.7c13.3-.2,21.6-9.6,21.6-23.8s-8.3-22.2-21.6-22.5h-59.7v46.2Z"/>
|
||||||
|
<path class="cls-2" d="M2082.2-1357.1c-.2,29.7-20.5,47.3-54.8,47.3s-57.8-18.2-57.8-45.4v-104.4h19v102.1c.2,18.8,14.1,29.1,37.7,29.1s36.6-10.3,36.8-29.1v-102.1h19v102.5h0Z"/>
|
||||||
|
<path class="cls-2" d="M2162-1328c17.8,0,30.8-6.8,30.8-21.4s-10.3-18.8-19.5-22c-11.8-4.1-21.4-5.6-35.1-9.6-17.1-5.1-29.7-19.7-29.7-40s15.6-42.6,47.7-42.6,38.1,4.1,55,22.5l-12.8,13.1c-15.2-14.8-34.2-17.3-43.2-17.3-17.3,0-27.8,11.1-27.8,23.5s6,19.3,20.1,23.3c7.3,2.1,18.6,5.4,34.4,10.1,23.5,7.1,29.7,21.4,29.7,38.7s-19.9,40-49.6,40-43.2-7.7-56.1-25.9l15.6-11.6c10.7,13.9,24.4,19.3,40.4,19.3Z"/>
|
||||||
|
<polygon class="cls-2" points="1660.9 -1459.9 1660.9 -1314.5 1671.6 -1314.5 1671.6 -1443.1 1713.3 -1443.1 1713.3 -1314.5 1733.8 -1314.5 1733.8 -1443.1 1776.7 -1443.1 1776.7 -1314.5 1802.4 -1314.5 1802.4 -1459.9 1660.9 -1459.9"/>
|
||||||
|
<polygon class="cls-2" points="1486.1 -1314.1 1405.3 -1314.1 1405.3 -1349.1 1367.5 -1349.1 1367.5 -1435.3 1405.9 -1435.3 1405.9 -1460.4 1449 -1460.4 1449 -1443.4 1422.9 -1443.4 1422.9 -1418.3 1384.5 -1418.3 1384.5 -1366.1 1422.3 -1366.1 1422.3 -1331.1 1486.1 -1331.1 1486.1 -1314.1"/>
|
||||||
|
<path class="cls-2" d="M2050.1-1548.9c-24.3,17-44.8,37.5-60.5,60h22.5c13.7-17.2,30.4-32.7,49.1-45.7v-21.6c-3.8,2.4-7.5,4.8-11.1,7.4Z"/>
|
||||||
|
<path class="cls-2" d="M2042.5-1579.8c6.1,4.5,12.4,8.7,18.8,12.6v-21.3c-2.8-1.8-5.5-3.7-8.1-5.7-16-11.8-29.8-25.3-41.5-40.3h-22.2c14.1,20.9,31.9,39.2,53,54.8Z"/>
|
||||||
|
<path class="cls-2" d="M1979.1-1634.6h-22.1c-13.9,18.4-31.1,34.2-50.8,47v21.1c29.5-17.1,54.4-40.3,72.9-68.1Z"/>
|
||||||
|
<path class="cls-2" d="M1928.3-1541.7c-7.1-5.4-14.5-10.3-22.1-14.9v21.3c3.8,2.5,7.5,5.2,11.2,7.9,14.8,11.3,27.8,24.1,39,38.4h22.3c-13.7-20-30.6-37.7-50.4-52.8Z"/>
|
||||||
|
<path class="cls-2" d="M1408.8-1192.9c0,10.2-8.2,18.5-18.5,18.5-10.2,0-18.5-8.2-18.5-18.5,0-10.2,8.2-18.5,18.5-18.5,10.2,0,18.5,8.3,18.5,18.5Z"/>
|
||||||
|
<path class="cls-2" d="M1445.8-1193c0,10.2-8.2,18.5-18.5,18.5-10.2,0-18.5-8.2-18.5-18.5,0-10.2,8.3-18.5,18.5-18.5,10.2,0,18.5,8.3,18.5,18.5Z"/>
|
||||||
|
<path class="cls-2" d="M1408.8-1155.9c0,10.2-8.3,18.5-18.5,18.5-10.2,0-18.5-8.2-18.5-18.5,0-10.2,8.3-18.5,18.5-18.5,10.2,0,18.5,8.2,18.5,18.5Z"/>
|
||||||
|
<path class="cls-2" d="M1445.8-1156c0,10.2-8.3,18.5-18.5,18.5-10.2,0-18.5-8.2-18.5-18.5,0-10.2,8.2-18.5,18.5-18.5,10.2,0,18.5,8.2,18.5,18.5Z"/>
|
||||||
|
<path class="cls-2" d="M1584.4-1182.4c0,26.3-19,43-46,42.4l-9-.2h-63.3v-145.5h69.8c27.2,1.1,38.9,18.2,38.9,38.5s-3.9,21-9.2,27c10.7,7.3,18.8,20.8,18.8,37.9ZM1484.2-1226.7h51.8c14.8,0,20.5-8.6,20.5-20.3s-5.6-20.5-24.2-20.5h-48.2v40.9ZM1566.2-1183.9c0-13.5-7.3-24.6-24.8-24.6h-57.1v50.1h55c19.7,0,27-10.5,27-25.5Z"/>
|
||||||
|
<path class="cls-2" d="M1620.8-1267.5v40.9h77.2v18.2h-77.2v50.1h85.8v18.2h-104v-145.5h104v18.2h-85.8Z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M1448.8-748.7c24.6.2,40.9,16,40.9,39.6s-15.6,43-38.9,43h-28.5l59,62.9h-25.2l-56.9-62.9h-12.6v62.9h-18.8v-145.5h81.1ZM1386.6-684.3h64c13.3-.2,21.6-9.6,21.6-23.8s-8.3-22.3-21.6-22.5h-64v46.2Z"/>
|
||||||
|
<path class="cls-2" d="M1603.5-651.4h-66.3l-19.5,48.1h-19.3l58.4-145.5h26.7l58.4,145.5h-19.3l-19.3-48.1ZM1596.2-669.5l-22.7-56.5-3.2-8.1-3.2,8.1-22.7,56.5h51.8Z"/>
|
||||||
|
<path class="cls-2" d="M1722.9-748.7c36.8.2,60.1,30.2,60.1,70.8s-19,74.7-61.4,74.7h-60.6v-145.5h61.8ZM1679.9-621.4h45.6c25.5,0,39.4-22,39.4-55.4s-16.1-53.5-40.2-53.7h-44.7v109.1h0Z"/>
|
||||||
|
<path class="cls-2" d="M1890.3-621v17.8h-94.1v-17.8h38.5v-110h-32.1v-17.8h81.3v17.8h-31.5v110h37.9Z"/>
|
||||||
|
<path class="cls-2" d="M1602.5-476.7h-66.3l-19.5,48.1h-19.3l58.4-145.5h26.7l58.4,145.5h-19.3l-19.3-48.1ZM1595.2-494.9l-22.7-56.5-3.2-8.1-3.2,8.1-22.7,56.5h51.8Z"/>
|
||||||
|
<path class="cls-2" d="M1911.3-573.8c24.6.2,40.9,16,40.9,39.6s-15.6,43-38.9,43h-59.9v62.9h-18.8v-145.5h76.8ZM1853.3-509.4h59.7c13.3-.2,21.6-9.6,21.6-23.8s-8.3-22.2-21.6-22.5h-59.7v46.2Z"/>
|
||||||
|
<path class="cls-2" d="M2082.6-471.3c-.2,29.7-20.5,47.3-54.8,47.3s-57.8-18.2-57.8-45.4v-104.4h19v102.1c.2,18.8,14.1,29.1,37.7,29.1s36.6-10.3,36.8-29.1v-102.1h19v102.5h0Z"/>
|
||||||
|
<path class="cls-2" d="M2162.5-442.2c17.8,0,30.8-6.8,30.8-21.4s-10.3-18.8-19.5-22c-11.8-4.1-21.4-5.6-35.1-9.6-17.1-5.1-29.7-19.7-29.7-40s15.6-42.6,47.7-42.6,38.1,4.1,55,22.5l-12.8,13.1c-15.2-14.8-34.2-17.3-43.2-17.3-17.3,0-27.8,11.1-27.8,23.5s6,19.3,20.1,23.3c7.3,2.1,18.6,5.4,34.4,10.1,23.5,7.1,29.7,21.4,29.7,38.7s-19.9,40-49.6,40-43.2-7.7-56.1-25.9l15.6-11.6c10.7,13.9,24.4,19.3,40.4,19.3Z"/>
|
||||||
|
<polygon class="cls-2" points="1661.4 -574.1 1661.4 -428.8 1672.1 -428.8 1672.1 -557.3 1713.8 -557.3 1713.8 -428.8 1734.2 -428.8 1734.2 -557.3 1777.2 -557.3 1777.2 -428.8 1802.8 -428.8 1802.8 -574.1 1661.4 -574.1"/>
|
||||||
|
<polygon class="cls-2" points="1486.6 -428.4 1405.8 -428.4 1405.8 -463.3 1368 -463.3 1368 -549.6 1406.4 -549.6 1406.4 -574.6 1449.5 -574.6 1449.5 -557.6 1423.4 -557.6 1423.4 -532.6 1385 -532.6 1385 -480.3 1422.8 -480.3 1422.8 -445.4 1486.6 -445.4 1486.6 -428.4"/>
|
||||||
|
<path class="cls-2" d="M2050.6-663.1c-24.3,17-44.8,37.5-60.5,60h22.5c13.7-17.2,30.4-32.7,49.1-45.7v-21.6c-3.8,2.4-7.5,4.8-11.1,7.4Z"/>
|
||||||
|
<path class="cls-2" d="M2042.9-694c6.1,4.5,12.4,8.7,18.8,12.6v-21.3c-2.8-1.8-5.5-3.7-8.1-5.7-16-11.8-29.8-25.3-41.5-40.3h-22.2c14.1,20.9,31.9,39.2,53,54.8Z"/>
|
||||||
|
<path class="cls-2" d="M1979.6-748.8h-22.1c-13.9,18.4-31.1,34.2-50.8,47v21.1c29.5-17.1,54.4-40.3,72.9-68.1Z"/>
|
||||||
|
<path class="cls-2" d="M1928.8-655.9c-7.1-5.4-14.5-10.3-22.1-14.9v21.3c3.8,2.5,7.5,5.2,11.2,7.9,14.8,11.3,27.8,24.1,39,38.4h22.3c-13.7-20-30.6-37.7-50.4-52.8Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M1409.3-307.2c0,10.2-8.2,18.5-18.5,18.5-10.2,0-18.5-8.2-18.5-18.5,0-10.2,8.3-18.5,18.5-18.5,10.2,0,18.5,8.3,18.5,18.5Z"/>
|
||||||
|
<path class="cls-2" d="M1446.3-307.2c0,10.2-8.2,18.5-18.5,18.5-10.2,0-18.5-8.2-18.5-18.5,0-10.2,8.3-18.5,18.5-18.5,10.2,0,18.5,8.2,18.5,18.5Z"/>
|
||||||
|
<path class="cls-2" d="M1409.3-270.2c0,10.2-8.3,18.5-18.5,18.5-10.2,0-18.5-8.2-18.5-18.5s8.2-18.5,18.5-18.5c10.2,0,18.5,8.2,18.5,18.5Z"/>
|
||||||
|
<circle class="cls-2" cx="1427.8" cy="-270.2" r="18.5"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M1584.9-296.6c0,26.3-19,43-46,42.4l-9-.2h-63.3v-145.5h69.8c27.2,1.1,38.9,18.2,38.9,38.5s-3.9,21-9.2,27c10.7,7.3,18.8,20.8,18.8,37.9ZM1484.7-340.9h51.8c14.8,0,20.5-8.6,20.5-20.3s-5.6-20.5-24.2-20.5h-48.1v40.9ZM1566.7-298.1c0-13.5-7.3-24.6-24.8-24.6h-57.1v50.1h55c19.7,0,27-10.5,27-25.5Z"/>
|
||||||
|
<path class="cls-2" d="M1621.3-381.8v40.9h77.2v18.2h-77.2v50.1h85.8v18.2h-104v-145.5h104v18.2h-85.8Z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M1993.6-346.7c0,69.6-16.9,97.2-53.1,97.2s-39.4-10.3-47.7-25.7l14.1-10.1c6.6,12.2,17.1,18,34.5,18s32.3-12.4,34.5-44.3c-9,7.9-21.2,13.1-34.7,13.1-31.7,0-52.2-24-52.2-48.2s20.8-56.9,52.2-56.9c31.3,0,52.4,24.2,52.4,56.9ZM1977.4-349.9c0-23.1-17.6-34.3-36.2-34.3s-34.7,11.1-34.7,34.3,14.8,33.4,34.7,33.4c20.8,0,36.2-13.5,36.2-33.4Z"/>
|
||||||
|
<path class="cls-2" d="M2109.2-272.2v18.4h-92.5v-18.4c57.4-42.6,71.7-60.8,71.7-83.7s-11.3-28.9-31.7-28.9-33,11.3-26.8,39.2l-17.1,7.9c-9.2-30.2,4.7-65.9,45.6-65.9s47.3,18.2,47.3,43.9-21,59.5-62.9,87.6h66.4Z"/>
|
||||||
|
<path class="cls-2" d="M2141.3-275.8v22h-22v-22h22Z"/>
|
||||||
|
<path class="cls-2" d="M2194.2-399.3h18.4v145.8h-18.4v-122.7l-39,37.9v-22.7l39-38.3Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M3011.2-678.5c.2-.1.4-.2.6-.4h-1.2c.2.1.4.2.6.4Z"/>
|
||||||
|
<path class="cls-2" d="M2744.4-663.2c-24.3,17-44.8,37.5-60.5,60h22.5c13.7-17.2,30.4-32.7,49.1-45.7v-21.6c-3.8,2.4-7.5,4.8-11.1,7.4Z"/>
|
||||||
|
<path class="cls-2" d="M2736.8-694.1c6.1,4.5,12.4,8.7,18.8,12.6v-21.3c-2.8-1.8-5.5-3.7-8.1-5.7-16-11.8-29.8-25.3-41.5-40.3h-22.2c14.1,20.9,31.9,39.2,53,54.8Z"/>
|
||||||
|
<path class="cls-2" d="M2673.4-748.9h-22.1c-13.9,18.4-31.1,34.2-50.8,47v21.1c29.5-17.1,54.4-40.3,72.8-68.1Z"/>
|
||||||
|
<path class="cls-2" d="M2622.6-656c-7.1-5.4-14.5-10.3-22.1-14.9v21.3c3.8,2.5,7.5,5.2,11.2,7.9,14.8,11.3,27.8,24.1,39,38.4h22.3c-13.7-20-30.6-37.7-50.4-52.8Z"/>
|
||||||
|
<polygon class="cls-2" points="2912.4 -602.8 2831.7 -602.8 2831.7 -637.7 2793.8 -637.7 2793.8 -723.9 2832.2 -723.9 2832.2 -749 2875.4 -749 2875.4 -732 2849.2 -732 2849.2 -706.9 2810.8 -706.9 2810.8 -654.7 2848.7 -654.7 2848.7 -619.8 2912.4 -619.8 2912.4 -602.8"/>
|
||||||
|
<polygon class="cls-2" points="2944.5 -748 2944.5 -602.6 2955.1 -602.6 2955.1 -731.2 2996.9 -731.2 2996.9 -602.6 3017.3 -602.6 3017.3 -731.2 3060.3 -731.2 3060.3 -602.6 3085.9 -602.6 3085.9 -748 2944.5 -748"/>
|
||||||
|
<path class="cls-2" d="M3162.6-733.1c0,10.5-8.5,19.1-19.1,19.1s-19.1-8.5-19.1-19.1,8.5-19.1,19.1-19.1,19.1,8.5,19.1,19.1Z"/>
|
||||||
|
<circle class="cls-2" cx="3258" cy="-733.1" r="19.1"/>
|
||||||
|
<path class="cls-2" d="M3200.7-695.1c0,10.5-8.5,19.1-19,19.1s-19.1-8.5-19.1-19c0-10.5,8.5-19.1,19-19.1,10.5,0,19.1,8.5,19.1,19Z"/>
|
||||||
|
<circle class="cls-2" cx="3219.8" cy="-695.1" r="19.1"/>
|
||||||
|
<path class="cls-2" d="M3200.8-656.9c0,10.5-8.5,19.1-19,19.1-10.5,0-19.1-8.5-19.1-19s8.5-19.1,19-19.1,19.1,8.5,19.1,19Z"/>
|
||||||
|
<circle class="cls-2" cx="3219.9" cy="-656.9" r="19.1"/>
|
||||||
|
<path class="cls-2" d="M3162.6-618.6c0,10.5-8.5,19.1-19.1,19.1s-19.1-8.5-19.1-19.1,8.5-19.1,19.1-19.1,19.1,8.5,19.1,19.1Z"/>
|
||||||
|
<circle class="cls-2" cx="3258" cy="-618.6" r="19.1"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M2765-1536.3c-27.7,19.4-51.1,42.7-69,68.4h25.6c15.6-19.6,34.6-37.3,56-52.1v-24.7c-4.3,2.7-8.5,5.5-12.7,8.4Z"/>
|
||||||
|
<path class="cls-2" d="M2756.2-1571.5c6.9,5.1,14.1,9.9,21.5,14.3v-24.3c-3.1-2.1-6.2-4.3-9.3-6.5-18.2-13.4-34-28.8-47.3-46h-25.3c16.1,23.8,36.3,44.8,60.4,62.5Z"/>
|
||||||
|
<path class="cls-2" d="M2683.9-1634h-25.2c-15.9,20.9-35.5,39-57.9,53.6v24.1c33.6-19.5,62-45.9,83.1-77.7Z"/>
|
||||||
|
<path class="cls-2" d="M2626-1528.1c-8.1-6.1-16.5-11.8-25.2-17v24.3c4.3,2.9,8.6,5.9,12.7,9.1,16.9,12.8,31.7,27.5,44.5,43.8h25.4c-15.7-22.8-34.9-43-57.5-60.2Z"/>
|
||||||
|
<polygon class="cls-2" points="2951.8 -1466.7 2859.7 -1466.7 2859.7 -1506.5 2816.6 -1506.5 2816.6 -1604.9 2860.3 -1604.9 2860.3 -1633.5 2909.6 -1633.5 2909.6 -1614.1 2879.7 -1614.1 2879.7 -1585.5 2835.9 -1585.5 2835.9 -1525.9 2879.1 -1525.9 2879.1 -1486.1 2951.8 -1486.1 2951.8 -1466.7"/>
|
||||||
|
<polygon class="cls-2" points="2600.8 -1442.6 2600.8 -1276.8 2612.9 -1276.8 2612.9 -1423.4 2660.5 -1423.4 2660.5 -1276.8 2683.9 -1276.8 2683.9 -1423.4 2732.9 -1423.4 2732.9 -1276.8 2762.1 -1276.8 2762.1 -1442.6 2600.8 -1442.6"/>
|
||||||
|
<path class="cls-2" d="M2854.6-1421c0,11.5-9.4,20.9-20.9,20.9s-20.9-9.4-20.9-20.9,9.4-20.9,20.9-20.9c11.5,0,20.9,9.4,20.9,20.9Z"/>
|
||||||
|
<circle class="cls-2" cx="2959.1" cy="-1421" r="20.9"/>
|
||||||
|
<path class="cls-2" d="M2896.4-1379.4c0,11.5-9.3,20.9-20.9,20.9-11.5,0-20.9-9.3-20.9-20.9,0-11.5,9.3-20.9,20.9-20.9,11.5,0,20.9,9.3,20.9,20.9Z"/>
|
||||||
|
<path class="cls-2" d="M2938.2-1379.5c0,11.5-9.3,20.9-20.9,20.9-11.5,0-20.9-9.3-20.9-20.9,0-11.5,9.3-20.9,20.9-20.9,11.5,0,20.9,9.3,20.9,20.9Z"/>
|
||||||
|
<path class="cls-2" d="M2896.5-1337.6c0,11.5-9.3,20.9-20.9,20.9-11.5,0-20.9-9.3-20.9-20.9,0-11.5,9.3-20.9,20.9-20.9,11.5,0,20.9,9.3,20.9,20.9Z"/>
|
||||||
|
<circle class="cls-2" cx="2917.4" cy="-1337.6" r="20.9"/>
|
||||||
|
<path class="cls-2" d="M2854.6-1295.6c0,11.5-9.4,20.9-20.9,20.9s-20.9-9.4-20.9-20.9,9.4-20.9,20.9-20.9c11.5,0,20.9,9.4,20.9,20.9Z"/>
|
||||||
|
<path class="cls-2" d="M2980-1295.6c0,11.5-9.4,20.9-20.9,20.9s-20.9-9.4-20.9-20.9,9.4-20.9,20.9-20.9c11.5,0,20.9,9.4,20.9,20.9Z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M215.5-1634c24.6.2,40.9,16,40.9,39.6s-15.6,43-38.9,43h-28.5l59,62.9h-25.2l-56.9-62.9h-12.6v62.9h-18.8v-145.5h81.1ZM153.2-1569.6h64c13.3-.2,21.6-9.6,21.6-23.8s-8.3-22.3-21.6-22.5h-64v46.2Z"/>
|
||||||
|
<path class="cls-2" d="M370.1-1536.7h-66.3l-19.5,48.1h-19.3l58.4-145.5h26.7l58.4,145.5h-19.3l-19.3-48.1ZM362.8-1554.9l-22.7-56.5-3.2-8.1-3.2,8.1-22.7,56.5h51.8Z"/>
|
||||||
|
<path class="cls-2" d="M489.5-1634c36.8.2,60.1,30.2,60.1,70.8s-19,74.7-61.4,74.7h-60.6v-145.5h61.8ZM446.5-1506.7h45.6c25.5,0,39.4-22,39.4-55.4s-16-53.5-40.2-53.7h-44.7v109.1h0Z"/>
|
||||||
|
<path class="cls-2" d="M656.9-1506.3v17.8h-94.1v-17.8h38.5v-110h-32.1v-17.8h81.3v17.8h-31.5v110h37.9Z"/>
|
||||||
|
</g>
|
||||||
|
<path class="cls-2" d="M369.1-1362h-66.3l-19.5,48.1h-19.3l58.4-145.5h26.7l58.4,145.5h-19.3l-19.3-48.1ZM361.8-1380.2l-22.7-56.5-3.2-8.1-3.2,8.1-22.7,56.5h51.8Z"/>
|
||||||
|
<path class="cls-2" d="M677.9-1459.1c24.6.2,40.9,16,40.9,39.6s-15.6,43-38.9,43h-59.9v62.9h-18.8v-145.5h76.8ZM619.9-1394.7h59.7c13.3-.2,21.6-9.6,21.6-23.8s-8.3-22.2-21.6-22.5h-59.7v46.2Z"/>
|
||||||
|
<path class="cls-2" d="M849.3-1356.6c-.2,29.7-20.5,47.3-54.8,47.3s-57.8-18.2-57.8-45.4v-104.4h19v102.1c.2,18.8,14.1,29.1,37.7,29.1s36.6-10.3,36.8-29.1v-102.1h19v102.5h0Z"/>
|
||||||
|
<path class="cls-2" d="M929.1-1327.5c17.8,0,30.8-6.8,30.8-21.4s-10.3-18.8-19.5-22c-11.8-4.1-21.4-5.6-35.1-9.6-17.1-5.1-29.7-19.7-29.7-40s15.6-42.6,47.7-42.6,38.1,4.1,55,22.5l-12.8,13.1c-15.2-14.8-34.2-17.3-43.2-17.3-17.3,0-27.8,11.1-27.8,23.5s6,19.3,20.1,23.3c7.3,2.1,18.6,5.4,34.4,10.1,23.5,7.1,29.7,21.4,29.7,38.7s-19.9,40-49.6,40-43.2-7.7-56.1-25.9l15.6-11.6c10.7,13.9,24.4,19.3,40.4,19.3Z"/>
|
||||||
|
<path class="cls-2" d="M535.8-1157.8v18.2h-103.2v-145.9h18.2v127.7h85Z"/>
|
||||||
|
<path class="cls-2" d="M253.1-1182c0,26.3-19.1,43-46,42.4l-9-.2h-63.4v-145.6h69.8c27.2,1.1,39,18.2,39,38.5s-3.9,21-9.2,27c10.7,7.3,18.8,20.8,18.8,37.9ZM152.9-1226.3h51.8c14.8,0,20.5-8.6,20.5-20.3s-5.6-20.6-24.2-20.6h-48.2v40.9h0ZM234.9-1183.5c0-13.5-7.3-24.6-24.8-24.6h-57.2v50.1h55c19.7,0,27-10.5,27-25.5Z"/>
|
||||||
|
<g>
|
||||||
|
<circle class="cls-2" cx="283.3" cy="-1267.3" r="18.3"/>
|
||||||
|
<circle class="cls-2" cx="393.3" cy="-1267.3" r="18.3"/>
|
||||||
|
<path class="cls-2" d="M338.3-1230.8c0,10.1-8.2,18.3-18.3,18.4-10.1,0-18.3-8.2-18.4-18.3,0-10.1,8.2-18.3,18.3-18.4,10.1,0,18.3,8.2,18.4,18.3Z"/>
|
||||||
|
<circle class="cls-2" cx="356.6" cy="-1230.8" r="18.3"/>
|
||||||
|
<path class="cls-2" d="M338.3-1194.2c0,10.1-8.2,18.3-18.3,18.4-10.1,0-18.3-8.2-18.4-18.3,0-10.1,8.2-18.3,18.3-18.4,10.1,0,18.3,8.2,18.4,18.3Z"/>
|
||||||
|
<circle class="cls-2" cx="356.6" cy="-1194.2" r="18.3"/>
|
||||||
|
<circle class="cls-2" cx="283.3" cy="-1157.3" r="18.3"/>
|
||||||
|
<circle class="cls-2" cx="393.3" cy="-1157.3" r="18.3"/>
|
||||||
|
</g>
|
||||||
|
<polygon class="cls-2" points="253.2 -1313.7 172.4 -1313.7 172.4 -1348.6 134.6 -1348.6 134.6 -1434.9 173 -1434.9 173 -1459.9 216.2 -1459.9 216.2 -1442.9 190 -1442.9 190 -1417.9 151.6 -1417.9 151.6 -1365.6 189.4 -1365.6 189.4 -1330.7 253.2 -1330.7 253.2 -1313.7"/>
|
||||||
|
<path class="cls-2" d="M756.8-1487.9h22.5c0,0,.1-.2.2-.3h-22.6c0,0-.1.2-.2.3Z"/>
|
||||||
|
<path class="cls-2" d="M673.4-1587.1v21.2c29.4-17.1,54.3-40.2,72.8-67.9h-22.2c-13.9,18.2-31,34-50.5,46.7Z"/>
|
||||||
|
<path class="cls-2" d="M828.7-1533.8v-21.7c-3.7,2.3-7.4,4.8-11.1,7.3-24.4,17.1-44.9,37.6-60.6,60.1h22.6c13.7-17.2,30.4-32.6,49.1-45.7Z"/>
|
||||||
|
<path class="cls-2" d="M695.5-1541c-7.1-5.4-14.5-10.4-22.1-14.9v21.3c3.8,2.5,7.5,5.2,11.2,7.9,14.8,11.3,27.9,24.2,39,38.5h22.4c-13.8-20.1-30.7-37.8-50.5-52.8Z"/>
|
||||||
|
<path class="cls-2" d="M820.6-1593.7c-15.9-11.7-29.7-25.1-41.3-40.1h-22.3c14.1,20.8,31.8,39.1,52.9,54.6,6.1,4.5,12.3,8.6,18.8,12.5v-21.4c-2.7-1.8-5.4-3.7-8.1-5.7Z"/>
|
||||||
|
<polygon class="cls-2" points="428 -1459.4 428 -1314.1 438.7 -1314.1 438.7 -1442.2 480.4 -1442.2 480.4 -1314.1 500.8 -1314.1 500.8 -1442.2 543.7 -1442.2 543.7 -1314.1 569.2 -1314.1 569.2 -1459.4 428 -1459.4"/>
|
||||||
|
</g>
|
||||||
|
<path class="cls-1" d="M1684.9,411c-11.5,0-20.9-9.3-20.9-20.9,0,11.5-9.3,20.9-20.9,20.9,11.5,0,20.9,9.3,20.9,20.9,0-11.5,9.3-20.9,20.9-20.9Z"/>
|
||||||
|
<path class="cls-2" d="M1646.7,283.4v-39.9h-43.1v-25.6c.2,0,.4-.1.6-.2-.2,0-.4-.1-.6-.2v-33.6h43.8v-28.6h29.8v-19.4h-49.2v28.6h-43.8v98.3h43.1v39.9h92.1v-19.4h-72.7Z"/>
|
||||||
|
<path class="cls-2" d="M1462.8,301.6h25.6c15.6-19.6,34.6-37.3,56-52.1v-24.7c-4.3,2.7-8.5,5.5-12.7,8.4-27.7,19.4-51.1,42.7-69,68.4Z"/>
|
||||||
|
<path class="cls-2" d="M1535.1,181.5c-18.2-13.4-34-28.8-47.3-46h-25.3c16.1,23.8,36.3,44.8,60.4,62.5,3.5,2.6,7.1,5.1,10.8,7.5,3.5,2.3,7.1,4.6,10.7,6.8v-24.3c-3.1-2.1-6.2-4.3-9.3-6.5Z"/>
|
||||||
|
<path class="cls-2" d="M1450.7,135.5h-25.2c-15.9,20.9-35.5,39-57.9,53.6v24.1c4.2-2.4,8.3-5,12.4-7.6,28.2-18.6,52.3-42.3,70.7-70.1Z"/>
|
||||||
|
<path class="cls-2" d="M1424.8,301.6h25.4c-15.7-22.8-34.9-43-57.5-60.2-8.1-6.1-16.5-11.8-25.2-17v24.3c4.3,2.9,8.6,5.9,12.7,9.1,16.9,12.8,31.7,27.5,44.5,43.8Z"/>
|
||||||
|
<polygon class="cls-2" points="1465.6 326.9 1447.3 326.9 1442.1 326.9 1369.8 326.9 1369.8 492.7 1382 492.7 1382 346.1 1429.6 346.1 1429.6 372 1429.6 492.7 1449.8 492.7 1452.9 492.7 1452.9 365.8 1452.9 347.6 1452.9 346.1 1460.7 346.1 1474.7 346.1 1501.9 346.1 1501.9 492.7 1531.1 492.7 1531.1 326.9 1470.8 326.9 1465.6 326.9"/>
|
||||||
|
<path class="cls-2" d="M1601.3,369.4c11.5,0,20.9-9.4,20.9-20.9s-9.4-20.9-20.9-20.9-20.9,9.4-20.9,20.9,9.4,20.9,20.9,20.9Z"/>
|
||||||
|
<circle class="cls-2" cx="1726.7" cy="348.5" r="20.9"/>
|
||||||
|
<path class="cls-2" d="M1664,390.1c0-11.5-9.4-20.9-20.9-20.9-11.5,0-20.9,9.4-20.9,20.9,0,11.5,9.4,20.9,20.9,20.9,11.5,0,20.9-9.4,20.9-20.9Z"/>
|
||||||
|
<circle class="cls-2" cx="1684.9" cy="390.1" r="20.9"/>
|
||||||
|
<path class="cls-2" d="M1643.1,411c-11.5,0-20.9,9.4-20.9,20.9,0,11.5,9.4,20.9,20.9,20.9,11.5,0,20.9-9.4,20.9-20.9,0-11.5-9.4-20.9-20.9-20.9Z"/>
|
||||||
|
<path class="cls-2" d="M1684.9,411c-11.5,0-20.9,9.4-20.9,20.9,0,11.5,9.4,20.9,20.9,20.9,11.5,0,20.9-9.4,20.9-20.9,0-11.5-9.4-20.9-20.9-20.9Z"/>
|
||||||
|
<path class="cls-2" d="M1601.3,453c-11.5,0-20.9,9.4-20.9,20.9s9.4,20.9,20.9,20.9,20.9-9.4,20.9-20.9c0-11.5-9.4-20.9-20.9-20.9Z"/>
|
||||||
|
<path class="cls-2" d="M1726.7,453c-11.5,0-20.9,9.4-20.9,20.9s9.4,20.9,20.9,20.9,20.9-9.4,20.9-20.9c0-11.5-9.4-20.9-20.9-20.9Z"/>
|
||||||
|
<path class="cls-2" d="M1900.6,139.4h-125.5c-5.5,0-10,5-10,11.1v85c0,6.1,4.5,11.1,10,11.1h18.5c.1,0,.2,0,.3,0h0s58.9,0,58.9,0h0c0,0,.1.2.1.3v19.2c0,.1,0,.2,0,.2h-30.1s0,0,0-.2v-10h-3.7v10c0,2.2,1.7,3.9,3.8,3.9h30.1c2.1,0,3.8-1.8,3.8-3.9v-19.2c0-.1,0-.2,0-.3h25.1c.1,0,.2,0,.3,0h18.5c5.5,0,10-5,10-11.1v-85c0-6.1-4.5-11.1-10-11.1ZM1790.1,238.2v-59.5c0-2.5,1.7-4.6,3.8-4.6h87.8c2.1,0,3.8,2.1,3.8,4.6v59.5c0,2.5-1.6,4.5-3.7,4.6h-8c.3-.8.5-1.7.5-2.7v-41.6c0-3.5-2.6-6.4-5.8-6.4h-61.5c-3.2,0-5.8,2.9-5.8,6.4v41.6c0,1,.2,1.9.5,2.7h-8c-2,0-3.7-2.1-3.7-4.6ZM1822.7,242.4v-20.4c0-.2,0-.3,0-.4h30.1s.1.1.1.3v20.4c0,.2,0,.3,0,.3h0s-30.1,0-30.1,0h0s-.1-.2-.1-.4ZM1852.9,218h-30.1c-2.1,0-3.8,1.8-3.8,4.1v20.4c0,.1,0,.2,0,.4h-2.7c-.5,0-.9-.6-.9-1.3v-29.1c0-.7.4-1.3.9-1.3h43c.5,0,.9.6.9,1.3v29.1c0,.7-.4,1.3-.9,1.3h0s-2.7,0-2.7,0c0-.1,0-.2,0-.4v-20.4c0-2.2-1.7-4.1-3.8-4.1ZM1859.4,207.3h-43c-2.6,0-4.6,2.2-4.6,5v29.1c0,.5,0,.9.2,1.3h-4.8c-1.2,0-2.1-1.2-2.1-2.7v-41.6c0-1.5,1-2.6,2.1-2.6h61.5c1.2,0,2.1,1.2,2.1,2.6v41.6c0,1.5-1,2.6-2.1,2.6h0s-4.8,0-4.8,0c.1-.4.2-.9.2-1.3v-29.1c0-2.8-2.1-5-4.6-5ZM1906.9,235.4c0,4.1-2.8,7.3-6.3,7.3h-12.5c.8-1.3,1.3-2.9,1.3-4.6v-59.5c0-4.6-3.4-8.3-7.6-8.3h-87.8c-4.2,0-7.6,3.7-7.6,8.3v59.5c0,1.7.5,3.3,1.3,4.6h-12.5c-3.5,0-6.3-3.3-6.3-7.3v-85c0-4.1,2.8-7.3,6.3-7.3h125.5c3.5,0,6.3,3.3,6.3,7.3v85h0Z"/>
|
||||||
|
<path class="cls-2" d="M2058,139h-126.3c-5.5,0-10,4.8-10,10.6v42.1h3.7v-42.1c0-3.8,2.8-6.9,6.3-6.9h126.3c3.5,0,6.3,3.1,6.3,6.9v80.9c0,3.3-2.8,7.1-6.3,7.1h-12.6c.8-1.3,1.2-2.8,1.2-4.4v-56.6c0-4.4-3.4-8-7.6-8h-88.4c-4.2,0-7.6,3.6-7.6,8v29.5h3.7v-29.5c0-2.4,1.7-4.3,3.9-4.3h88.4c2.1,0,3.9,1.9,3.9,4.3v56.6c0,2.4-1.8,4.4-3.9,4.4h0s-7.9,0-7.9,0c.3-.8.5-1.6.5-2.5v-39.6c0-3.4-2.6-6.1-5.9-6.1h-61.9c-3.2,0-5.9,2.8-5.9,6.1v20.6h3.7v-20.6c0-1.3,1-2.4,2.2-2.4h61.9c1.2,0,2.2,1.1,2.2,2.4v39.6c0,1.3-.9,2.4-1.9,2.5h-5c0-.4.2-.8.2-1.2v-27.7c0-2.7-2.1-4.9-4.7-4.9h-43.3c-2.6,0-4.7,2.2-4.7,4.9v14.4h3.7v-14.4c0-.6.4-1.1,1-1.1h43.3c.5,0,.9.5.9,1.1v27.7c0,.6-.4,1.2-.9,1.2h0s-2.7,0-2.7,0c0-.1,0-.2,0-.3v-19.4c0-2.2-1.7-4-3.8-4h-30.3c-2.1,0-3.8,1.8-3.8,4v10.1h3.7v-10.1c0-.2,0-.2.1-.2h30.3s.1,0,.1.2v19.4c0,.2,0,.3-.1.3h0s-30.1,0-30.1,0v3.7s45.9,0,45.9,0h0c.2,0,.4,0,.6,0h31.6c5.4,0,10-5,10-10.8v-80.9c0-5.9-4.5-10.6-10-10.6Z"/>
|
||||||
|
<path class="cls-2" d="M2010.1,261c0,.2,0,.2-.1.2h-30.3s-.1,0-.1-.2v-19.4c0-.2,0-.3.1-.3v-3.7c-2.1,0-3.8,1.8-3.8,4v19.4c0,2.2,1.7,4,3.8,4h30.3c2.1,0,3.8-1.8,3.8-4v-10.1h-3.7v10.1Z"/>
|
||||||
|
<path class="cls-2" d="M2225,336.8h-3.7v-186c0-3.8-2.8-6.9-6.2-6.9h-124.4c-3.4,0-6.2,3.1-6.2,6.9h-3.7c0-5.8,4.5-10.6,9.9-10.6h124.4c5.5,0,9.9,4.7,9.9,10.6v186Z"/>
|
||||||
|
<path class="cls-2" d="M2203.9,307.6h-3.7v-130.2c0-2.3-1.7-4.3-3.8-4.3h-87.1c-2.1,0-3.8,1.9-3.8,4.3h-3.7c0-4.4,3.4-8,7.5-8h87.1c4.1,0,7.5,3.6,7.5,8v130.2Z"/>
|
||||||
|
<path class="cls-2" d="M2189.2,287.2h-3.7v-91.2c0-1.3-.9-2.4-2.1-2.4h-61c-1.2,0-2.1,1.1-2.1,2.4h-3.7c0-3.4,2.6-6.1,5.8-6.1h61c3.2,0,5.8,2.8,5.8,6.1v91.2h0Z"/>
|
||||||
|
<path class="cls-2" d="M2178.8,272.8h-3.7v-63.8c0-.6-.4-1.1-.9-1.1h-42.7c-.5,0-.9.5-.9,1.1h-3.7c0-2.7,2.1-4.9,4.6-4.9h42.7c2.6,0,4.6,2.2,4.6,4.9v63.8h0Z"/>
|
||||||
|
<path class="cls-2" d="M2171.6,262.8h-3.7v-44.7c0-.2,0-.2,0-.2h-29.9s0,0,0,.2h-3.7c0-2.2,1.7-4,3.8-4h29.9c2.1,0,3.8,1.8,3.8,4v44.7Z"/>
|
||||||
|
<path class="cls-2" d="M2137.8,265.9c-2.1,0-3.8-1.8-3.8-4h3.7c0,.2,0,.3.1.3v3.7Z"/>
|
||||||
|
<path class="cls-2" d="M2131.5,276.2c-2.5,0-4.6-2.2-4.6-4.9h3.7c0,.6.4,1.2.9,1.2v3.7h0Z"/>
|
||||||
|
<path class="cls-2" d="M2122.3,291.6c-3.2,0-5.8-2.8-5.8-6.3h3.7c0,1.4.9,2.5,2.1,2.5v3.7h0Z"/>
|
||||||
|
<path class="cls-2" d="M2109.4,312.3c-4.1,0-7.5-3.7-7.5-8.1h3.7c0,2.4,1.7,4.4,3.7,4.4v3.7h0Z"/>
|
||||||
|
<path class="cls-2" d="M2090.4,344.1c-5.5,0-9.9-4.9-9.9-10.9h3.7c0,4,2.8,7.2,6.2,7.2v3.7Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M1157.8,206.6v9.8h-55.5v-78.5h9.8v68.7h45.7Z"/>
|
||||||
|
<path class="cls-2" d="M1005.1,193.6c0,14.2-10.2,23.1-24.8,22.8h-4.8c0-.1-34.1-.1-34.1-.1v-78.3h37.5c14.6.6,21,9.8,21,20.7s-2.1,11.3-5,14.5c5.8,3.9,10.1,11.2,10.1,20.4ZM951.2,169.7h27.9c7.9,0,11.1-4.6,11.1-10.9s-3-11.1-13-11.1h-25.9v22h0ZM995.3,192.8c0-7.3-3.9-13.2-13.4-13.2h-30.8v26.9h29.6c10.6,0,14.5-5.6,14.5-13.7Z"/>
|
||||||
|
<circle class="cls-2" cx="1021.5" cy="147.7" r="9.9" transform="translate(643.1 1108) rotate(-76.7)"/>
|
||||||
|
<circle class="cls-2" cx="1080.7" cy="147.7" r="9.9"/>
|
||||||
|
<circle class="cls-2" cx="1041.3" cy="167.3" r="9.9"/>
|
||||||
|
<path class="cls-2" d="M1070.8,167.3c0,5.4-4.4,9.9-9.8,9.9-5.4,0-9.9-4.4-9.9-9.8,0-5.4,4.4-9.9,9.8-9.9,5.4,0,9.9,4.4,9.9,9.8Z"/>
|
||||||
|
<circle class="cls-2" cx="1041.3" cy="187" r="9.9"/>
|
||||||
|
<circle class="cls-2" cx="1061" cy="187" r="9.9"/>
|
||||||
|
<circle class="cls-2" cx="1021.5" cy="206.8" r="9.9" transform="translate(585.5 1153.5) rotate(-76.7)"/>
|
||||||
|
<circle class="cls-2" cx="1080.7" cy="206.8" r="9.9"/>
|
||||||
|
<path class="cls-2" d="M574.6,189.6h-35.7l-10.5,25.9h-10.4l31.4-78.3h14.4l31.4,78.3h-10.4l-10.4-25.9ZM570.7,179.8l-12.2-30.4-1.7-4.4-1.7,4.4-12.2,30.4h27.9Z"/>
|
||||||
|
<path class="cls-2" d="M740.7,137.3c13.2.1,22,8.6,22,21.3s-8.4,23.1-21,23.1h-32.2v33.8h-10.1v-78.3h41.3ZM709.5,172h32.1c7.1-.1,11.6-5.2,11.6-12.8s-4.5-12-11.6-12.1h-32.1v24.9Z"/>
|
||||||
|
<path class="cls-2" d="M832.9,192.5c-.1,16-11.1,25.4-29.5,25.4s-31.1-9.8-31.1-24.4v-56.2h10.2v54.9c.1,10.1,7.6,15.7,20.3,15.7s19.7-5.5,19.8-15.7v-54.9h10.2v55.1Z"/>
|
||||||
|
<path class="cls-2" d="M875.9,208.1c9.6,0,16.6-3.7,16.6-11.5s-5.5-10.1-10.5-11.9c-6.3-2.2-11.5-3-18.9-5.2-9.2-2.8-16-10.6-16-21.5s8.4-22.9,25.7-22.9,20.5,2.2,29.6,12.1l-6.9,7c-8.2-7.9-18.4-9.3-23.3-9.3-9.3,0-15,6-15,12.7s3.2,10.4,10.8,12.5c3.9,1.2,10,2.9,18.5,5.4,12.7,3.8,16,11.5,16,20.8s-10.7,21.5-26.7,21.5-23.3-4.1-30.2-13.9l8.4-6.2c5.8,7.5,13.1,10.4,21.8,10.4Z"/>
|
||||||
|
<polygon class="cls-2" points="512.3 215.6 468.8 215.6 468.8 196.8 448.4 196.8 448.4 150.4 469.1 150.4 469.1 136.9 492.3 136.9 492.3 146 478.2 146 478.2 159.5 457.6 159.5 457.6 187.6 477.9 187.6 477.9 206.4 512.3 206.4 512.3 215.6"/>
|
||||||
|
<path class="cls-2" d="M73.2,137.2c13.2.1,22,8.6,22,21.3s-8.4,23.1-21,23.1h-15.3l31.8,33.9h-13.6l-30.6-33.9h-6.8v33.8h-10.1v-78.3h43.6ZM39.7,171.9h34.4c7.1-.1,11.6-5.2,11.6-12.8s-4.5-12-11.6-12.1h-34.4v24.9Z"/>
|
||||||
|
<path class="cls-2" d="M156.4,189.6h-35.7l-10.5,25.9h-10.4l31.4-78.3h14.4l31.4,78.3h-10.4l-10.4-25.9ZM152.5,179.8l-12.2-30.4-1.7-4.4-1.7,4.4-12.2,30.4h27.9Z"/>
|
||||||
|
<path class="cls-2" d="M220.7,137.2c19.8.1,32.3,16.2,32.3,38.1s-10.2,40.2-33,40.2h-32.6v-78.3h33.3ZM197.5,205.7h24.5c13.7,0,21.2-11.9,21.2-29.8s-8.6-28.8-21.6-28.9h-24.1v58.7Z"/>
|
||||||
|
<path class="cls-2" d="M310.7,205.9v9.6h-50.6v-9.6h20.7v-59.2h-17.3v-9.6h43.7v9.6h-16.9v59.2h20.4Z"/>
|
||||||
|
<polygon class="cls-2" points="612 215.7 612 146.2 634.5 146.2 634.5 215.7 645.5 215.7 645.5 146.2 668.6 146.2 668.6 215.7 682.4 215.7 682.4 136.7 606.3 136.7 606.3 215.7 612 215.7"/>
|
||||||
|
<path class="cls-2" d="M319.6,162.4v11.4c15.8-9.2,29.1-21.6,39.1-36.5h-11.9c-7.5,9.8-16.6,18.2-27.1,25.1Z"/>
|
||||||
|
<path class="cls-2" d="M403,191v-11.7c-2,1.3-4,2.6-5.9,3.9-13.1,9.2-24.2,20.2-32.6,32.4h12.1c7.4-9.3,16.4-17.6,26.5-24.7Z"/>
|
||||||
|
<path class="cls-2" d="M319.6,190.6c2,1.4,4,2.8,6,4.3,8,6.1,15,13,21,20.7h12c-7.4-10.8-16.5-20.3-27.1-28.4-3.8-2.9-7.8-5.6-11.9-8v11.4h0Z"/>
|
||||||
|
<path class="cls-2" d="M403,161.9c-1.5-1-2.9-2-4.3-3-8.5-6.3-15.9-13.5-22.2-21.5h-12c7.6,11.1,17.1,21,28.4,29.3,3.2,2.4,6.6,4.6,10.1,6.7v-11.5h0Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="small">
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M605.8,679.6h-35.7l-10.5,25.9h-10.4l31.4-78.3h14.4l31.4,78.3h-10.4l-10.4-25.9ZM601.8,669.8l-12.2-30.4-1.7-4.4-1.7,4.4-12.2,30.4h27.9Z"/>
|
||||||
|
<path class="cls-2" d="M771.9,627.3c13.2.1,22,8.6,22,21.3s-8.4,23.1-21,23.1h-32.2v33.8h-10.1v-78.3h41.3ZM740.7,662h32.1c7.1-.1,11.6-5.2,11.6-12.8s-4.5-12-11.6-12.1h-32.1v24.9Z"/>
|
||||||
|
<path class="cls-2" d="M864.1,682.5c-.1,16-11.1,25.4-29.5,25.4s-31.1-9.8-31.1-24.4v-56.2h10.2v54.9c.1,10.1,7.6,15.7,20.3,15.7s19.7-5.5,19.8-15.7v-54.9h10.2v55.1Z"/>
|
||||||
|
<path class="cls-2" d="M907,698.1c9.6,0,16.6-3.7,16.6-11.5s-5.5-10.1-10.5-11.9c-6.3-2.2-11.5-3-18.9-5.2-9.2-2.8-16-10.6-16-21.5s8.4-22.9,25.7-22.9,20.5,2.2,29.6,12.1l-6.9,7c-8.2-7.9-18.4-9.3-23.3-9.3-9.3,0-15,6-15,12.7s3.2,10.4,10.8,12.5c3.9,1.2,10,2.9,18.5,5.4,12.7,3.8,16,11.5,16,20.8s-10.7,21.5-26.7,21.5-23.3-4.1-30.2-13.9l8.4-6.2c5.8,7.5,13.1,10.4,21.8,10.4Z"/>
|
||||||
|
<polygon class="cls-2" points="543.4 705.6 500 705.6 500 686.8 479.6 686.8 479.6 640.4 500.3 640.4 500.3 626.9 523.5 626.9 523.5 636 509.4 636 509.4 649.5 488.7 649.5 488.7 677.6 509.1 677.6 509.1 696.4 543.4 696.4 543.4 705.6"/>
|
||||||
|
<path class="cls-2" d="M104.4,627.2c13.2.1,22,8.6,22,21.3s-8.4,23.1-21,23.1h-15.3l31.8,33.9h-13.6l-30.6-33.9h-6.8v33.8h-10.1v-78.3h43.6ZM70.9,661.9h34.4c7.1-.1,11.6-5.2,11.6-12.8s-4.5-12-11.6-12.1h-34.4v24.9Z"/>
|
||||||
|
<path class="cls-2" d="M187.6,679.6h-35.7l-10.5,25.9h-10.4l31.4-78.3h14.4l31.4,78.3h-10.4l-10.4-25.9ZM183.7,669.8l-12.2-30.4-1.7-4.4-1.7,4.4-12.2,30.4h27.9Z"/>
|
||||||
|
<path class="cls-2" d="M251.8,627.2c19.8.1,32.3,16.2,32.3,38.1s-10.2,40.2-33,40.2h-32.6v-78.3h33.3ZM228.7,695.7h24.5c13.7,0,21.2-11.9,21.2-29.8s-8.6-28.8-21.6-28.9h-24.1v58.7h0Z"/>
|
||||||
|
<path class="cls-2" d="M341.9,695.9v9.6h-50.6v-9.6h20.7v-59.2h-17.3v-9.6h43.7v9.6h-16.9v59.2h20.4Z"/>
|
||||||
|
<polygon class="cls-2" points="643.2 705.7 643.2 636.2 665.7 636.2 665.7 705.7 676.7 705.7 676.7 636.2 699.8 636.2 699.8 705.7 713.6 705.7 713.6 626.7 637.4 626.7 637.4 705.7 643.2 705.7"/>
|
||||||
|
<path class="cls-2" d="M350.8,652.4v11.4c15.8-9.2,29.1-21.6,39.1-36.5h-11.9c-7.5,9.8-16.6,18.2-27.1,25.1Z"/>
|
||||||
|
<path class="cls-2" d="M434.1,681v-11.7c-2,1.3-4,2.6-5.9,3.9-13.1,9.2-24.2,20.2-32.6,32.4h12.1c7.4-9.3,16.4-17.6,26.5-24.7Z"/>
|
||||||
|
<path class="cls-2" d="M350.8,680.6c2,1.4,4,2.8,6,4.3,8,6,15,13,21,20.7h12c-7.4-10.8-16.5-20.3-27.1-28.4-3.8-2.9-7.8-5.6-11.9-8v11.5h0Z"/>
|
||||||
|
<path class="cls-2" d="M434.1,651.9c-1.5-1-2.9-2-4.3-3-8.5-6.3-15.9-13.5-22.2-21.5h-12c7.6,11.1,17.1,21,28.4,29.3,3.2,2.4,6.6,4.6,10.1,6.7v-11.5Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M963.9,677.9c0,5.5-4.4,10-9.9,10-5.5,0-10-4.4-10-9.9,0-5.5,4.4-10,9.9-10,5.5,0,10,4.4,10,9.9Z"/>
|
||||||
|
<circle class="cls-2" cx="973.8" cy="677.8" r="10"/>
|
||||||
|
<circle class="cls-2" cx="954" cy="697.8" r="10"/>
|
||||||
|
<circle class="cls-2" cx="973.9" cy="697.8" r="10"/>
|
||||||
|
<path class="cls-2" d="M1058.4,683.5c0,14.2-10.2,23.1-24.8,22.8h-4.8c0-.1-34.1-.1-34.1-.1v-78.3h37.5c14.6.6,21,9.8,21,20.7s-2.1,11.3-5,14.5c5.8,3.9,10.1,11.2,10.1,20.4ZM1004.5,659.7h27.9c7.9,0,11.1-4.6,11.1-10.9s-3-11.1-13-11.1h-25.9v22h0ZM1048.6,682.7c0-7.3-3.9-13.2-13.4-13.2h-30.7v26.9h29.6c10.6,0,14.5-5.6,14.5-13.7Z"/>
|
||||||
|
<path class="cls-2" d="M1077.9,637.7v22h41.6v9.8h-41.6v26.9h46.2v9.8h-55.9v-78.3h55.9v9.8h-46.2Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 33 KiB |
69
radiocampus/static/radiocampus/logos/logo-RC-maquette.svg
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1190.6 841.9">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #000;
|
||||||
|
stroke-width: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M80,318.5c14.5.1,24.2,9.5,24.2,23.4s-9.2,25.4-23,25.4h-16.8l34.9,37.2h-14.9l-33.6-37.2h-7.4v37.2h-11.1v-86h47.9,0ZM43.2,356.6h37.8c7.9-.1,12.8-5.7,12.8-14.1s-4.9-13.2-12.8-13.3h-37.8v27.3h0Z"/>
|
||||||
|
<path class="cls-1" d="M171.4,376.1h-39.2l-11.5,28.4h-11.4l34.5-86h15.8l34.5,86h-11.4l-11.4-28.4h.1ZM167.1,365.4l-13.4-33.4-1.9-4.8-1.9,4.8-13.4,33.4h30.6,0Z"/>
|
||||||
|
<path class="cls-1" d="M242,318.5c21.8.1,35.5,17.9,35.5,41.9s-11.2,44.2-36.3,44.2h-35.8v-86h36.6ZM216.6,393.8h27c15.1,0,23.3-13,23.3-32.7s-9.5-31.6-23.8-31.7h-26.4v64.5h0Z"/>
|
||||||
|
<path class="cls-1" d="M341,394v10.5h-55.6v-10.5h22.8v-65h-19v-10.5h48.1v10.5h-18.6v65h22.4,0Z"/>
|
||||||
|
<path class="cls-1" d="M170.9,479.3h-39.2l-11.5,28.4h-11.4l34.5-86h15.8l34.5,86h-11.4l-11.4-28.4h.1ZM166.5,468.6l-13.4-33.4-1.9-4.8-1.9,4.8-13.4,33.4h30.6,0Z"/>
|
||||||
|
<path class="cls-1" d="M353.4,421.9c14.5.1,24.2,9.5,24.2,23.4s-9.2,25.4-23,25.4h-35.4v37.2h-11.1v-86h45.4,0ZM319.1,460h35.3c7.9-.1,12.8-5.7,12.8-14.1s-4.9-13.1-12.8-13.3h-35.3v27.3h0Z"/>
|
||||||
|
<path class="cls-1" d="M454.7,482.5c-.1,17.6-12.1,28-32.4,28s-34.2-10.8-34.2-26.8v-61.7h11.2v60.4c.1,11.1,8.3,17.2,22.3,17.2s21.6-6.1,21.8-17.2v-60.4h11.2v60.6h0Z"/>
|
||||||
|
<path class="cls-1" d="M501.9,499.7c10.5,0,18.2-4,18.2-12.7s-6.1-11.1-11.5-13c-7-2.4-12.6-3.3-20.7-5.7-10.1-3-17.6-11.6-17.6-23.6s9.2-25.2,28.2-25.2,22.5,2.4,32.5,13.3l-7.6,7.7c-9-8.7-20.2-10.2-25.5-10.2-10.2,0-16.4,6.6-16.4,13.9s3.5,11.4,11.9,13.8c4.3,1.2,11,3.2,20.3,6,13.9,4.2,17.6,12.7,17.6,22.9s-11.8,23.6-29.3,23.6-25.5-4.6-33.2-15.3l9.2-6.9c6.3,8.2,14.4,11.4,23.9,11.4h0Z"/>
|
||||||
|
<polygon class="cls-1" points="205.7 421.8 205.7 507.6 212 507.6 212 431.7 236.6 431.7 236.6 507.6 248.7 507.6 248.7 431.7 274.1 431.7 274.1 507.6 289.3 507.6 289.3 421.8 205.7 421.8"/>
|
||||||
|
<polygon class="cls-1" points="102.3 507.9 54.6 507.9 54.6 487.3 32.2 487.3 32.2 436.2 54.9 436.2 54.9 421.5 80.4 421.5 80.4 431.5 65 431.5 65 446.3 42.3 446.3 42.3 477.2 64.6 477.2 64.6 497.8 102.3 497.8 102.3 507.9"/>
|
||||||
|
<path class="cls-1" d="M435.7,369.1c-14.4,10-26.5,22.2-35.8,35.5h13.3c8.1-10.2,18-19.3,29-27v-12.8c-2.2,1.4-4.4,2.8-6.6,4.4h0Z"/>
|
||||||
|
<path class="cls-1" d="M431.2,350.9c3.6,2.7,7.3,5.1,11.1,7.4v-12.6c-1.7-1.1-3.3-2.2-4.8-3.4-9.5-7-17.6-15-24.5-23.8h-13.1c8.3,12.4,18.9,23.2,31.3,32.4h0Z"/>
|
||||||
|
<path class="cls-1" d="M393.8,318.5h-13.1c-8.2,10.9-18.4,20.2-30,27.8v12.5c17.4-10.1,32.2-23.8,43.1-40.3Z"/>
|
||||||
|
<path class="cls-1" d="M363.7,373.4c-4.2-3.2-8.6-6.1-13.1-8.8v12.6c2.2,1.5,4.4,3.1,6.6,4.7,8.7,6.7,16.4,14.2,23.1,22.7h13.2c-8.1-11.8-18.1-22.3-29.8-31.2h0Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M56.7,579.5c0,6-4.8,10.9-10.9,10.9s-10.9-4.8-10.9-10.9,4.9-10.9,10.9-10.9,10.9,4.9,10.9,10.9Z"/>
|
||||||
|
<path class="cls-1" d="M78.5,579.5c0,6-4.8,10.9-10.9,10.9s-10.9-4.8-10.9-10.9,4.9-10.9,10.9-10.9,10.9,4.8,10.9,10.9Z"/>
|
||||||
|
<path class="cls-1" d="M56.7,601.4c0,6-4.9,10.9-10.9,10.9s-10.9-4.8-10.9-10.9,4.8-10.9,10.9-10.9,10.9,4.8,10.9,10.9Z"/>
|
||||||
|
<circle class="cls-1" cx="67.6" cy="601.4" r="10.9"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M160.5,585.8c0,15.5-11.2,25.4-27.2,25.1h-5.3c0-.1-37.4-.1-37.4-.1v-86h41.3c16.1.7,23,10.8,23,22.8s-2.3,12.4-5.4,16c6.3,4.3,11.1,12.3,11.1,22.4h0ZM101.2,559.6h30.6c8.7,0,12.1-5.1,12.1-12s-3.3-12.1-14.3-12.1h-28.4v24.2h0ZM149.7,584.9c0-8-4.3-14.5-14.7-14.5h-33.8v29.6h32.5c11.6,0,16-6.2,16-15.1h0Z"/>
|
||||||
|
<path class="cls-1" d="M182,535.4v24.2h45.6v10.8h-45.6v29.6h50.7v10.8h-61.5v-86h61.5v10.8h-50.7Z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M402,556.2c0,41.1-10,57.5-31.4,57.5s-23.3-6.1-28.2-15.2l8.3-6c3.9,7.2,10.1,10.6,20.4,10.6s19.1-7.3,20.4-26.2c-5.3,4.7-12.5,7.7-20.5,7.7-18.7,0-30.9-14.2-30.9-28.5s12.3-33.6,30.9-33.6,31,14.3,31,33.6h0ZM392.5,554.3c0-13.7-10.4-20.3-21.4-20.3s-20.5,6.6-20.5,20.3,8.7,19.7,20.5,19.7,21.4-8,21.4-19.7Z"/>
|
||||||
|
<path class="cls-1" d="M470.4,600.2v10.9h-54.7v-10.9c33.9-25.2,42.4-35.9,42.4-49.5s-6.7-17.1-18.7-17.1-19.5,6.7-15.8,23.2l-10.1,4.7c-5.4-17.9,2.8-39,27-39s28,10.8,28,26-12.4,35.2-37.2,51.8h39.3Z"/>
|
||||||
|
<path class="cls-1" d="M489.4,598.1v13h-13v-13h13Z"/>
|
||||||
|
<path class="cls-1" d="M520.6,525.1h10.9v86.2h-10.9v-72.5l-23.1,22.4v-13.4l23.1-22.6Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M1157.8,206.6v9.8h-55.5v-78.5h9.8v68.7h45.7Z"/>
|
||||||
|
<path class="cls-1" d="M1005.1,193.6c0,14.2-10.2,23.1-24.8,22.8h-4.8c0-.1-34.1-.1-34.1-.1v-78.3h37.5c14.6.6,21,9.8,21,20.7s-2.1,11.3-5,14.5c5.8,3.9,10.1,11.2,10.1,20.4h0ZM951.2,169.7h27.9c7.9,0,11.1-4.6,11.1-10.9s-3-11.1-13-11.1h-25.9v22h0ZM995.3,192.8c0-7.3-3.9-13.2-13.4-13.2h-30.8v26.9h29.6c10.6,0,14.5-5.6,14.5-13.7h.1Z"/>
|
||||||
|
<circle class="cls-1" cx="1021.8" cy="147.9" r="9.9" transform="translate(642.9 1108.3) rotate(-76.7)"/>
|
||||||
|
<circle class="cls-1" cx="1080.7" cy="147.7" r="9.9"/>
|
||||||
|
<circle class="cls-1" cx="1041.3" cy="167.3" r="9.9"/>
|
||||||
|
<path class="cls-1" d="M1070.8,167.3c0,5.4-4.4,9.9-9.8,9.9s-9.9-4.4-9.9-9.8,4.4-9.9,9.8-9.9,9.9,4.4,9.9,9.8Z"/>
|
||||||
|
<circle class="cls-1" cx="1041.3" cy="187" r="9.9"/>
|
||||||
|
<circle class="cls-1" cx="1061" cy="187" r="9.9"/>
|
||||||
|
<circle class="cls-1" cx="1021.7" cy="207" r="9.9" transform="translate(585.3 1153.7) rotate(-76.7)"/>
|
||||||
|
<circle class="cls-1" cx="1080.7" cy="206.8" r="9.9"/>
|
||||||
|
<path class="cls-1" d="M574.6,189.6h-35.7l-10.5,25.9h-10.4l31.4-78.3h14.4l31.4,78.3h-10.4l-10.4-25.9h.2ZM570.7,179.8l-12.2-30.4-1.7-4.4-1.7,4.4-12.2,30.4h27.9,0Z"/>
|
||||||
|
<path class="cls-1" d="M740.7,137.3c13.2.1,22,8.6,22,21.3s-8.4,23.1-21,23.1h-32.2v33.8h-10.1v-78.3h41.3ZM709.5,172h32.1c7.1-.1,11.6-5.2,11.6-12.8s-4.5-12-11.6-12.1h-32.1v24.9h0Z"/>
|
||||||
|
<path class="cls-1" d="M832.9,192.5c0,16-11.1,25.4-29.5,25.4s-31.1-9.8-31.1-24.4v-56.2h10.2v54.9c0,10.1,7.6,15.7,20.3,15.7s19.7-5.5,19.8-15.7v-54.9h10.2v55.1h0Z"/>
|
||||||
|
<path class="cls-1" d="M875.9,208.1c9.6,0,16.6-3.7,16.6-11.5s-5.5-10.1-10.5-11.9c-6.3-2.2-11.5-3-18.9-5.2-9.2-2.8-16-10.6-16-21.5s8.4-22.9,25.7-22.9,20.5,2.2,29.6,12.1l-6.9,7c-8.2-7.9-18.4-9.3-23.3-9.3-9.3,0-15,6-15,12.7s3.2,10.4,10.8,12.5c3.9,1.2,10,2.9,18.5,5.4,12.7,3.8,16,11.5,16,20.8s-10.7,21.5-26.7,21.5-23.3-4.1-30.2-13.9l8.4-6.2c5.8,7.5,13.1,10.4,21.8,10.4h.1Z"/>
|
||||||
|
<polygon class="cls-1" points="512.3 215.6 468.8 215.6 468.8 196.8 448.4 196.8 448.4 150.4 469.1 150.4 469.1 136.9 492.3 136.9 492.3 146 478.2 146 478.2 159.5 457.6 159.5 457.6 187.6 477.9 187.6 477.9 206.4 512.3 206.4 512.3 215.6"/>
|
||||||
|
<path class="cls-1" d="M73.2,137.2c13.2.1,22,8.6,22,21.3s-8.4,23.1-21,23.1h-15.3l31.8,33.9h-13.6l-30.6-33.9h-6.8v33.8h-10.1v-78.3h43.6ZM39.7,171.9h34.4c7.1-.1,11.6-5.2,11.6-12.8s-4.5-12-11.6-12.1h-34.4v24.9h0Z"/>
|
||||||
|
<path class="cls-1" d="M156.4,189.6h-35.7l-10.5,25.9h-10.4l31.4-78.3h14.4l31.4,78.3h-10.4l-10.4-25.9h.2ZM152.5,179.8l-12.2-30.4-1.7-4.4-1.7,4.4-12.2,30.4h27.9-.1Z"/>
|
||||||
|
<path class="cls-1" d="M220.7,137.2c19.8.1,32.3,16.2,32.3,38.1s-10.2,40.2-33,40.2h-32.6v-78.3h33.3ZM197.5,205.7h24.5c13.7,0,21.2-11.9,21.2-29.8s-8.6-28.8-21.6-28.9h-24.1v58.7h0Z"/>
|
||||||
|
<path class="cls-1" d="M310.7,205.9v9.6h-50.6v-9.6h20.7v-59.2h-17.3v-9.6h43.7v9.6h-16.9v59.2h20.4,0Z"/>
|
||||||
|
<polygon class="cls-1" points="612 215.7 612 146.2 634.5 146.2 634.5 215.7 645.5 215.7 645.5 146.2 668.6 146.2 668.6 215.7 682.4 215.7 682.4 136.7 606.3 136.7 606.3 215.7 612 215.7"/>
|
||||||
|
<path class="cls-1" d="M319.6,162.4v11.4c15.8-9.2,29.1-21.6,39.1-36.5h-11.9c-7.5,9.8-16.6,18.2-27.1,25.1h-.1Z"/>
|
||||||
|
<path class="cls-1" d="M403,191v-11.7c-2,1.3-4,2.6-5.9,3.9-13.1,9.2-24.2,20.2-32.6,32.4h12.1c7.4-9.3,16.4-17.6,26.5-24.7h-.1Z"/>
|
||||||
|
<path class="cls-1" d="M319.6,190.6c2,1.4,4,2.8,6,4.3,8,6.1,15,13,21,20.7h12c-7.4-10.8-16.5-20.3-27.1-28.4-3.8-2.9-7.8-5.6-11.9-8v11.4h0Z"/>
|
||||||
|
<path class="cls-1" d="M403,161.9c-1.5-1-2.9-2-4.3-3-8.5-6.3-15.9-13.5-22.2-21.5h-12c7.6,11.1,17.1,21,28.4,29.3,3.2,2.4,6.6,4.6,10.1,6.7v-11.5h0Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 7.9 KiB |
561
radiocampus/static/radiocampus/radiocampus.css
Normal file
@ -0,0 +1,561 @@
|
|||||||
|
:root {
|
||||||
|
--a-player-bar-bg: #738EF2;
|
||||||
|
--a-player-bar-fg: #4897c7;
|
||||||
|
--a-player-url-fg: white;
|
||||||
|
--a-playlist-header-bg: #F6ED80;;
|
||||||
|
--a-playlist-header-fg: #222;
|
||||||
|
--a-player-panel-bg: #738ef2;
|
||||||
|
--a-player-panel-fg: white;
|
||||||
|
--a-sound-hv-bg: #f6ed80;
|
||||||
|
--a-sound-hv-fg: #444;
|
||||||
|
--a-sound-bg: #f6ed80;
|
||||||
|
--body-bg: unset;
|
||||||
|
--break-color: transparent;
|
||||||
|
--button-bg: #e9e9ed;
|
||||||
|
--button-fg: #222;
|
||||||
|
--button-hv-bg: #F4F88D;
|
||||||
|
--button-hv-fg: #1d3cab;
|
||||||
|
--button-active-fg: white;
|
||||||
|
--button-active-bg: #738ef2;
|
||||||
|
--cover-small-h: 10rem;
|
||||||
|
--cover-small-w: 10rem;
|
||||||
|
--heading-font-family: "campus_grotesk";
|
||||||
|
--header-height: 320px;
|
||||||
|
--heading-link-hv-fg: #aa217b;
|
||||||
|
--heading-hg-fg: #fff;
|
||||||
|
--link-fg: #3b47ff;
|
||||||
|
--link-hv-fg: #c40c85;
|
||||||
|
--main-color-light: #F4F881;
|
||||||
|
--nav-bg: transparent;
|
||||||
|
--nav-fg: #222;
|
||||||
|
--nav-secondary-bg: transparent;
|
||||||
|
--nav-hv-bg: unset;
|
||||||
|
--nav-active-bg: unset;
|
||||||
|
--nav-active-fg: white;
|
||||||
|
--preview-title-sz: 21px;
|
||||||
|
--text-color: #75124e;
|
||||||
|
--text-color-light: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "campus_grotesk";
|
||||||
|
src:
|
||||||
|
local("campus_grotesk"),
|
||||||
|
url("/static/radiocampus/fonts/campus_grotesk/CampusGroteskv24-Regular.woff2") format("woff2"),
|
||||||
|
url("/static/radiocampus/fonts/campus_grotesk/CampusGroteskv24-Regular.woff") format("woff"),
|
||||||
|
url("/static/radiocampus/fonts/campus_grotesk/CampusGroteskv24-Regular.otf") format("opentype") tech(color-COLRv1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: #222;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
body.home, body.home .preview .headings a, body.home .page a {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
body.home .preview .headings a:hover {
|
||||||
|
color: #f4f88d !important;
|
||||||
|
}
|
||||||
|
body.home .nav.primary {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
body.yellow {
|
||||||
|
background: url(/static/radiocampus/backgrounds/degrade-jaune.jpg) repeat-x top fixed;
|
||||||
|
}
|
||||||
|
body.yellow :root {
|
||||||
|
--nav-active-fg: #f4f88d;
|
||||||
|
}
|
||||||
|
body.yellow.home {
|
||||||
|
background: url(/static/radiocampus/backgrounds/photo-degrade-02.jpg) no-repeat center/cover;
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
body.blue {
|
||||||
|
background: url(/static/radiocampus/backgrounds/degrade-bleu.jpg) repeat-x top fixed;
|
||||||
|
}
|
||||||
|
body.blue.home {
|
||||||
|
background: url(/static/radiocampus/backgrounds/photo-degrade-01.jpg) no-repeat center/cover;
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
body.blue #grandlogo img {
|
||||||
|
content: url('/static/radiocampus/logos/logo-RC-blanc1.png');
|
||||||
|
}
|
||||||
|
body.yellow #grandlogo img {
|
||||||
|
content: url('/static/radiocampus/logos/logo-RC-bleu1.png');
|
||||||
|
}
|
||||||
|
body.blue.home #grandlogo img {
|
||||||
|
content: url('/static/radiocampus/logos/logo-RC-blanc2.png');
|
||||||
|
}
|
||||||
|
body.yellow.home #grandlogo img {
|
||||||
|
content: url('/static/radiocampus/logos/logo-RC-bleu2.png');
|
||||||
|
}
|
||||||
|
body.yellow .nav .nav-item.active {
|
||||||
|
color: #738EF2 !important;
|
||||||
|
text-shadow: -3px 3px 17px rgb(0, 48, 111);
|
||||||
|
}
|
||||||
|
body.blue #grandlogo img, body.yellow #grandlogo img {
|
||||||
|
width: 120px;
|
||||||
|
margin: 12px 0 0 48px;
|
||||||
|
}
|
||||||
|
body.blue.home #grandlogo, body.yellow.home #grandlogo {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
body.blue.home #grandlogo img , body.yellow.home #grandlogo img {
|
||||||
|
margin: 12px auto 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-player-bar {
|
||||||
|
border-top: 1px solid #555;
|
||||||
|
}
|
||||||
|
.a-player .button, .a-player-bar-content {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
#player .a-sound-item .label {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
.a-switch-nav span {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.a-switch-nav span:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
a.heading.title {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.heading.title:hover {
|
||||||
|
/*color: var(--link-hv-fg); */
|
||||||
|
color: #738ef2;
|
||||||
|
}
|
||||||
|
.button, a.button, button.button {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
.header-cover:not(:only-child) {
|
||||||
|
float: left;
|
||||||
|
margin: 0 1.2rem 1.2rem 0;
|
||||||
|
}
|
||||||
|
.header.has-cover {
|
||||||
|
min-height: unset;
|
||||||
|
}
|
||||||
|
.item-section {display:flex; align-items:end}
|
||||||
|
.fifty {
|
||||||
|
width: 55%;
|
||||||
|
}
|
||||||
|
.grid.list-emissions {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||||
|
grid-auto-flow: dense;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
.grid.list-emissions .media-content {
|
||||||
|
font-size: 0.84rem;
|
||||||
|
}
|
||||||
|
.today {
|
||||||
|
color: yellow;
|
||||||
|
font-size: 1.4em !important;
|
||||||
|
}
|
||||||
|
.list-grille article {
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
}
|
||||||
|
.lagrille .category {
|
||||||
|
color: white;
|
||||||
|
margin-left: 10px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.lagrille:not(.homedisplay) .heading.subtitle {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.mt-3 {
|
||||||
|
margin-top: unset !important;
|
||||||
|
}
|
||||||
|
.nav.primary .nav-brand {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.nav.secondary .nav-item {
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
a.nav-item:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.nav.primary .nav-item {
|
||||||
|
font-weight: unset;
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
.page section.container {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.program-list {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-home {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.list-home article div.media {
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.list-home article.active div.media div.media-content a, .list-home article.active div.media div.media-content span {
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-home article.active div.media a:before {
|
||||||
|
content: "\2192";
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-home article div.media a.preview-cover {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.list-home article div.media div.media-content.flex-column {
|
||||||
|
display: unset;
|
||||||
|
}
|
||||||
|
.list-home article div.media div.media-content section.content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.list-home article div.media div.media-content div.episode-date {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.list-home article div.media div.media-content span.heading.subtitle {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.schedule {
|
||||||
|
background-color: unset;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
padding: 0 0.2rem 0.2rem 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedules {
|
||||||
|
margin-bottom: 0.4rem !important;
|
||||||
|
}
|
||||||
|
.title, .preview .title, .preview .title:not(:last-child) {
|
||||||
|
text-transform: unset;
|
||||||
|
font-weight: unset;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
:root {
|
||||||
|
--header-height: 200px;
|
||||||
|
}
|
||||||
|
.page {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: var(--nav-primary-height);
|
||||||
|
}
|
||||||
|
.nav.primary .nav-brand {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.nav.secondary .nav-item {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
.dropdown-trigger .icon, .icon.bullet {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.dropdown.is-right .dropdown-menu {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.grid.list-emissions {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
#grandlogo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.navs, .nav-menu.active {
|
||||||
|
background-color: #7892f1; /*#738ef2;*/
|
||||||
|
}
|
||||||
|
.nav .nav-item {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
.nav .nav-item.active {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
.navs .nav + .nav {
|
||||||
|
flex-grow: 1 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1408px) {
|
||||||
|
.container:not(.is-max-desktop):not(.is-max-widescreen) {
|
||||||
|
max-width: unset;
|
||||||
|
margin: 10px 64px;
|
||||||
|
}
|
||||||
|
body.home .container:not(.is-max-desktop):not(.is-max-widescreen) {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1216px) {
|
||||||
|
.container:not(.is-max-desktop):not(.is-max-widescreen) {
|
||||||
|
max-width: unset;
|
||||||
|
margin: 0 64px;
|
||||||
|
}
|
||||||
|
body.home .container:not(.is-max-desktop):not(.is-max-widescreen) {
|
||||||
|
max-width: 1152px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
.grid.list-emissions {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item .headings {
|
||||||
|
margin-bottom: .2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:nth-child(3n+1):not(.wide) .media {
|
||||||
|
border-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:nth-child(3n):not(.wide) .media {
|
||||||
|
border-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:not(.wide) .media {
|
||||||
|
padding: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-content .content p, .episode-date {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
.nav-urls .urls a, .nav-urls .urls span {
|
||||||
|
color: white;
|
||||||
|
background-color: #444;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#background {
|
||||||
|
background-size: cover;
|
||||||
|
padding: 80px 0 80px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.button:hover, a.button:hover, button.button:hover {
|
||||||
|
opacity: 0.9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: whitesmoke;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 5px;
|
||||||
|
max-width: 960px;
|
||||||
|
border: 1px solid #929293;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.breadcrumbs, .container.header {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0 24px 0 24px;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.breadcrumbs a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid .list-item .headings .heading {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav.secondary {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav.secondary .nav-item{
|
||||||
|
olor: #1d3cab !important;
|
||||||
|
color: #8c827e !important;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav .nav-item:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navs {
|
||||||
|
border-bottom: 1px solid #4d4545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-cover {
|
||||||
|
border-radius: 5px;
|
||||||
|
opacity: 0.92;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-carousel .preview-cover {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-card .headings .heading {
|
||||||
|
opacity: 0.85;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #242121;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-card .headings a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid .preview .headings a {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview .title, .preview .title:not(:last-child) {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
.nav .nav-menu {
|
||||||
|
background-color: #6C7ED2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#background {
|
||||||
|
padding: 100px 0 50px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container, .page .container {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* fred fixes */
|
||||||
|
.grid.list-emissions:not(.list-home) {display: flex !important;flex-wrap: wrap !important; gap: 20px 1rem;}
|
||||||
|
|
||||||
|
.grid.list-emissions:not(.list-home) > .list-item {width: calc(20% - 2rem) !important ;}
|
||||||
|
|
||||||
|
.grid.list-emissions:not(.list-home) > .list-item .preview-cover {max-width: 100% !important;
|
||||||
|
display: block;
|
||||||
|
width: 100% !important;
|
||||||
|
min-width: auto;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
height: fit-content;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
background-color: white;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid.list-emissions:not(.list-home) .media-content {margin-top: 0;}
|
||||||
|
|
||||||
|
.grid.list-emissions:not(.list-home) > .list-item .media .media-content > br, .grid.listfive > .list-item .media .media-content .episode-date > br {display: none;}
|
||||||
|
|
||||||
|
.grid.list-emissions:not(.list-home) .list-item .subtitle {text-align: left !important;font-size: 0.8rem !important;}
|
||||||
|
|
||||||
|
|
||||||
|
.grid.list-emissions:not(.list-home) .preview.active .heading:not(:empty) {
|
||||||
|
color: #738EF2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid.list-podcasts {display: flex !important;flex-wrap: wrap !important; gap: 20px 1rem;}
|
||||||
|
|
||||||
|
.grid.list-podcasts > .list-item {width: calc(50% - 2rem) !important ;}
|
||||||
|
|
||||||
|
.grid.list-podcasts > .list-item .preview-cover {max-width: 100% !important;
|
||||||
|
display: block;
|
||||||
|
width: 50% !important;
|
||||||
|
min-width: auto;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
height: fit-content;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid.list-podcasts .media-content {margin-top: 0;}
|
||||||
|
|
||||||
|
.grid.list-podcasts .media .media-left {width: 25% !important;}
|
||||||
|
.grid.list-podcasts .media .media-content {width: 75%;}
|
||||||
|
|
||||||
|
.grid.list-podcasts > .list-item .media .media-content > br, .grid.list-emissions > .list-item .media .media-content .episode-date > br {display: none;}
|
||||||
|
|
||||||
|
.grid.list-podcasts .list-item .subtitle {text-align: left !important;font-size: 0.8rem !important;}
|
||||||
|
|
||||||
|
|
||||||
|
.grid.list-podcasts .preview.active .heading:not(:empty) {
|
||||||
|
color: #738EF2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1224px) {
|
||||||
|
.grid.list-emissions:not(.list-home) > .list-item {width: calc(33% - 2rem) !important;;}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 924px) {
|
||||||
|
.grid.list-emissions:not(.list-home) > .list-item {width: calc(50% - 2rem) !important;}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
.grid.list-emissions > .list-item {width: 100% !important;}
|
||||||
|
.program-list {flex-direction: column;}
|
||||||
|
.grid.list-podcasts > .list-item {width: 100% !important ;}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item .subtitle:not(:empty) {
|
||||||
|
min-width: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 924px) {
|
||||||
|
.a-player-bar-content {overflow: hidden;}
|
||||||
|
.a-player-bar-content .title {
|
||||||
|
display: inline-block;
|
||||||
|
padding-right: 2em;
|
||||||
|
padding-left: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
animation: defilement-rtl 55s infinite linear;
|
||||||
|
overflow: visible;}
|
||||||
|
|
||||||
|
@keyframes defilement-rtl {
|
||||||
|
0% {
|
||||||
|
transform: translate3d(0,0,0); /* position initiale à droite */
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate3d(-100%,0,0); /* position finale à gauche */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 540px) {
|
||||||
|
.media {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.grid.list-podcasts .media .media-content {width:100%}
|
||||||
|
|
||||||
|
.grid.list-emissions:not(.list-home) > .list-item {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item .media-content {height:auto;}
|