-
+ |
{% translate "Data source" %}
|
diff --git a/assets/package.json b/assets/package.json
index 2b99bc2..663bb66 100644
--- a/assets/package.json
+++ b/assets/package.json
@@ -10,8 +10,10 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.0.0",
+ "@popperjs/core": "^2.11.8",
"core-js": "^3.8.3",
"lodash": "^4.17.21",
+ "v-calendar": "^3.1.2",
"vue": "^3.2.13"
},
"devDependencies": {
@@ -20,7 +22,7 @@
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
- "bulma": "^0.9.3",
+ "bulma": "^0.9.4",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"sass": "^1.49.9",
diff --git a/assets/src/admin.js b/assets/src/admin.js
index df2305e..7d60734 100644
--- a/assets/src/admin.js
+++ b/assets/src/admin.js
@@ -1,4 +1,3 @@
-import './assets/styles.scss'
import './assets/admin.scss'
import './index.js'
diff --git a/assets/src/app.js b/assets/src/app.js
index ea37962..daa4e6e 100644
--- a/assets/src/app.js
+++ b/assets/src/app.js
@@ -1,9 +1,16 @@
+import {Calendar, DatePicker} from 'v-calendar';
import components from './components'
const App = {
el: '#app',
delimiters: ['[[', ']]'],
- components: {...components},
+ components: {
+ ...components,
+ ...{
+ VCalendar: Calendar,
+ VDatepicker: DatePicker
+ },
+ },
computed: {
player() { return window.aircox.player; },
diff --git a/assets/src/appBuilder.js b/assets/src/appBuilder.js
deleted file mode 100644
index 4546ba3..0000000
--- a/assets/src/appBuilder.js
+++ /dev/null
@@ -1,139 +0,0 @@
-import {createApp} from 'vue'
-
-/**
- * Utility class used to handle Vue applications. It provides way to load
- * remote application and update history.
- */
-export default class Builder {
- constructor(config={}) {
- this.config = config
- this.title = null
- this.app = null
- this.vm = null
- }
-
- /**
- * Fetch app from remote and mount application.
- */
- fetch(url, {el='#app', ...options}={}) {
- return fetch(url, options).then(response => response.text())
- .then(content => {
- let doc = new DOMParser().parseFromString(content, 'text/html')
- let app = doc.querySelector(el)
- content = app ? app.innerHTML : content
- return this.mount({content, title: doc.title, reset:true, url })
- })
- }
-
- /**
- * Mount application, using `create_app` if required.
- *
- * @param {String} options.content: replace app container content with it
- * @param {String} options.title: set DOM document title.
- * @param {String} [options.el=this.config.el]: mount application on this element (querySelector argument)
- * @param {Boolean} [reset=False]: if True, force application recreation.
- * @return `app.mount`'s result.
- */
- mount({content=null, title=null, el=null, reset=false, props=null}={}) {
- try {
- this.unmount()
-
- let config = this.config
- if(el === null)
- el = config.el
- if(reset || !this.app)
- this.app = this.createApp({title,content,el,...config}, props)
-
- this.vm = this.app.mount(el)
- window.scroll(0, 0)
- return this.vm
- } catch(error) {
- this.unmount()
- throw error
- }
- }
-
- createApp({el, title=null, content=null, ...config}, props) {
- const container = document.querySelector(el)
- if(!container)
- return
- if(content)
- container.innerHTML = content
- if(title)
- document.title = title
- return createApp(config, props)
- }
-
- unmount() {
- this.app && this.app.unmount()
- this.app = null
- this.vm = null
- }
-
- /**
- * Enable hot reload: catch page change in order to fetch them and
- * load page without actually leaving current one.
- */
- enableHotReload(node=null, historySave=true) {
- if(historySave)
- this.historySave(document.location, true)
- node.addEventListener('click', event => this.pageChanged(event), true)
- node.addEventListener('submit', event => this.pageChanged(event), true)
- node.addEventListener('popstate', event => this.statePopped(event), true)
- }
-
- 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'))
- 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.fetch(url, options).then(() => this.historySave(url))
- event.preventDefault();
- event.stopPropagation();
- }
-
- statePopped(event) {
- const state = event.state
- if(state && state.content)
- // document.title = this.title;
- this.historyLoad(state);
- }
-
- /// Save application state into browser history
- historySave(url,replace=false) {
- const el = document.querySelector(this.config.el)
- const state = {
- content: el.innerHTML,
- title: document.title,
- }
-
- if(replace)
- history.replaceState(state, '', url)
- else
- history.pushState(state, '', url)
- }
-
- /// Load application from browser history's state
- historyLoad(state) {
- return this.mount({ content: state.content, title: state.title })
- }
-}
diff --git a/assets/src/assets/admin.scss b/assets/src/assets/admin.scss
index 10b6f4b..35b9262 100644
--- a/assets/src/assets/admin.scss
+++ b/assets/src/assets/admin.scss
@@ -1,5 +1,77 @@
+@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;
}
diff --git a/assets/src/assets/common.scss b/assets/src/assets/common.scss
new file mode 100644
index 0000000..fad9a46
--- /dev/null
+++ b/assets/src/assets/common.scss
@@ -0,0 +1,88 @@
+@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);
+
+ --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: 18px !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: 24px !important; }
+}
+
+h1, h2, h3, h4, h5, h6, .heading, .title, .subtitle {
+ font-family: var(--heading-font-family);
+}
+
+
+.container:empty {
+ display: none;
+}
diff --git a/assets/src/assets/components.scss b/assets/src/assets/components.scss
new file mode 100644
index 0000000..9182200
--- /dev/null
+++ b/assets/src/assets/components.scss
@@ -0,0 +1,703 @@
+@use "vars" as v;
+
+:root {
+ --title-1-sz: 1.6rem;
+ --title-2-sz: 1.4rem;
+ --title-3-sz: 1.2rem;
+ --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: 14rem;
+ --cover-h: 14rem;
+ --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-3-sz);
+ --preview-subtitle-sz: var(--title-3-sz);
+ --preview-cover-size: 14rem;
+ --preview-cover-small-size: 10rem;
+ --preview-cover-tiny-size: 4rem;
+ --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};
+ }
+}
+
+// ---- headings
+.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);
+ }
+ }
+}
+
+
+// ---- button
+@mixin button {
+ .button, a.button, button.button {
+ font-size: v.$text-size;
+ display: inline-block;
+ padding: v.$mp-2;
+ border: none; //1px var(--button-fg) solid;
+ justify-content: center;
+ text-align: center;
+ // font-size: v.$text-size-medium;
+ cursor: pointer;
+ text-decoration: none;
+
+ color: var(--button-fg);
+ background-color: var(--button-bg);
+
+ &.secondary {
+ background-color: var(--button-sec-bg);
+ }
+
+ .label, label {
+ cursor: pointer;
+ }
+
+ .icon {
+ vertical-align: middle;
+ &:not(:only-child) {
+ &:first-child { margin-right: v.$mp-3; }
+ &:last-child { margin-left: v.$mp-3 }
+ }
+ }
+
+ &: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; }
+ }
+ }
+}
+
+
+// ---- 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 {
+ .content {
+ font-size: v.$text-size;
+ }
+ }
+
+}
+
+
+.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(--preview-cover-small-size);
+ width: var(--preview-cover-small-size) !important;
+ min-width: var(--preview-cover-small-size);
+ }
+
+ &.tiny, .preview.tiny & {
+ min-width: unset;
+ height: var(--preview-cover-tiny-size);
+ width: var(--preview-cover-tiny-size) !important;
+ min-width: var(--preview-cover-tiny-size);
+ }
+}
+
+.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(--preview-cover-small-size);
+ }
+ }
+
+ .actions {
+ text-align: right;
+ }
+
+ &: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;
+ }
+ }
diff --git a/assets/src/assets/helpers.scss b/assets/src/assets/helpers.scss
new file mode 100644
index 0000000..cded0d8
--- /dev/null
+++ b/assets/src/assets/helpers.scss
@@ -0,0 +1,92 @@
+@use "./vars" as v;
+
+// ---- layout
+.align-left { text-align: left; justify-content: left; }
+.align-right { text-align: right; justify-content: right; }
+
+.clear-left { clear: left !important }
+.clear-right { clear: right !important }
+.clear-both { clear: both !important }
+
+.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; }
+
+// ---- 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; }
+
+.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-transparent { background-color: transparent; }
+
+.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;
+}
diff --git a/assets/src/assets/public.scss b/assets/src/assets/public.scss
new file mode 100644
index 0000000..24e05d6
--- /dev/null
+++ b/assets/src/assets/public.scss
@@ -0,0 +1,478 @@
+@use "./vars" as v;
+@use "./components";
+
+@use "./vendor";
+
+
+// ---- 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 {
+ 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;
+ border-bottom: 1px var(--main-color) solid;
+
+ .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) {
+ 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: 1000;
+ 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 {
+ with: 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;
+ }
+}
diff --git a/assets/src/assets/styles.scss b/assets/src/assets/styles.scss
deleted file mode 100644
index b7d6e2a..0000000
--- a/assets/src/assets/styles.scss
+++ /dev/null
@@ -1,327 +0,0 @@
-@charset "utf-8";
-
-@import "~bulma/sass/utilities/_all.sass";
-@import "~bulma/sass/components/dropdown.sass";
-
-$body-background-color: $light;
-$menu-item-hover-background-color: #dfdfdf;
-$menu-item-active-background-color: #d2d2d2;
-
-@import "~bulma";
-
-//-- helpers/modifiers
-.is-fullwidth { width: 100%; }
-.is-fullheight { height: 100%; }
-.is-fixed-bottom {
- position: fixed;
- bottom: 0;
- margin-bottom: 0px;
- border-radius: 0;
-}
-.is-borderless { border: none; }
-
-.has-text-nowrap {
- white-space: nowrap;
-}
-
-.has-background-transparent {
- background-color: transparent;
-}
-
-.is-opacity-light {
- opacity: 0.7;
- &:hover {
- opacity: 1;
- }
-}
-
-.float-right { float: right }
-.float-left { float: left }
-.overflow-hidden { overflow: hidden }
-.overflow-hidden.is-fullwidth { max-width: 100%; }
-
-
-*[draggable="true"] {
- cursor: move;
-}
-
-//-- forms
-input.half-field:not(:active):not(:hover) {
- border: none;
- background-color: rgba(0,0,0,0);
- cursor: pointer;
-}
-
-
-//-- animations
-@keyframes blink {
- from { opacity: 1; }
- to { opacity: 0.4; }
-}
-
-.blink {
- animation: 1s ease-in-out 3s infinite alternate blink;
-}
-
-
-//-- navbar
-.navbar + .container {
- margin-top: 1em;
-}
-
-.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 {
- .navbar-dropdown {
- z-index: 2000;
- }
-
- .navbar-split {
- margin: 0.2em 0em;
- margin-right: 1em;
- padding-right: 1em;
- border-right: 1px $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 $grey-light solid;
-
- font-size: $size-5;
- color: $text-light;
- font-weight: $weight-light;
- }
- }
-}
-
-//-- cards
-.card {
- .title {
- a {
- color: $dark;
- }
-
- padding: 0.2em;
- font-size: $size-5;
- font-weight: $weight-medium;
- }
-
- &.is-primary {
- box-shadow: 0em 0em 0.5em $black
- }
-}
-
-.card-super-title {
- position: absolute;
- z-index: 1000;
- font-size: $size-6;
- font-weight: $weight-bold;
- padding: 0.2em;
- top: 1em;
- background-color: #ffffffc7;
- max-width: 90%;
-
- .fas {
- padding: 0.1em;
- font-size: 0.8em;
- }
-}
-
-
-//-- page
-.page {
- & > .cover {
- float: right;
- max-width: 45%;
- }
-
- .header {
- margin-bottom: 1.5em;
- }
-
- .headline {
- font-size: 1.4em;
- padding: 0.2em 0em;
- }
-
- p { padding: 0.4em 0em; }
- hr { background-color: $grey-light; }
-
- .page-content {
- h1 { font-size: $size-1; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
- h2 { font-size: $size-3; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
- h3 { font-size: $size-4; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
- h4 { font-size: $size-5; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
- h5 { font-size: $size-6; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
- h6 { font-size: $size-6; margin-top:0.4em; margin-bottom:0.2em; }
- }
-}
-
-
-.media.item .headline {
- line-height: 1.2em;
- max-height: calc(1.2em * 3);
- overflow: hidden;
-
- & + .headline-overflow {
- position: relative;
- width: 100%;
- height: 2em;
- margin-top: -2em;
- }
-
- & + .headline-overflow:before {
- content:'';
- width:100%;
- height:100%;
- position:absolute;
- left:0;
- bottom:0;
- background:linear-gradient(transparent 1em, $body-background-color);
- }
-}
-
-
-//-- player
-.player {
- z-index: 10000;
- box-shadow: 0em 1.5em 2.5em rgba(0, 0, 0, 0.6);
-
- .player-panels {
- height: 0%;
- transition: height 3s;
- }
- .player-panels.is-open {
- height: auto;
- }
-
- .player-panel {
- margin: 0.4em;
- max-height: 80%;
- overflow-y: auto;
- }
-
- .progress {
- margin: 0em;
- padding: 0em;
- border-color: $info;
- border-style: 'solid';
- }
-
- .player-bar {
- border-top: 1px $grey-light solid;
-
- > div {
- height: 3.75em !important;
- }
-
- > .media-left:not(:last-child) {
- margin-right: 0em;
- }
-
- > .media-cover {
- border-left: 1px black solid;
- }
-
- .cover {
- font-size: 1.5rem !important;
- height: 2.5em !important;
- }
-
- > .media-content {
- padding-top: 0.4em;
- padding-left: 0.4em;
- }
-
- .button {
- font-size: 1.5rem !important;
- height: 100%;
- padding: auto 0.2em !important;
- min-width: 2.5em;
- border-radius: 0px;
- transition: background-color 1s;
- }
-
- .title {
- margin: 0em;
- }
- }
-
-}
-
-//-- media
-.media {
- .subtitle {
- margin-bottom: 0.4em;
- }
- .media-content .headline {
- font-size: 1em;
- font-weight: 400;
- }
-}
-
-//-- general
-body {
- background-color: $body-background-color;
-}
-
-section > .toolbar {
- background-color: rgba(0,0,0,0.05);
- padding: 1em;
- margin-bottom: 1.5em;
-}
-
-
-main {
- .cover.is-small { width: 10em; }
- .cover.is-tiny { height: 2em; }
-}
-
-
-aside {
- & > section {
- margin-bottom: 2em;
- }
-
- .cover.is-small { width: 10em; }
- .cover.is-tiny { height: 2em; }
-
- .media .subtitle {
- font-size: 1em;
- }
-}
-
-.sound-item {
- .cover { height: 5em; }
- .media-content a { padding: 0em; }
- margin-bottom: 0.2em;
-}
-.sound-item .media-right .button {
- margin-right: 0.2em;
- min-width: 2.5em;
- display: inline-block;
-}
-
-
-.timetable {
- width: 100%;
- border: none;
-}
diff --git a/assets/src/assets/vars.scss b/assets/src/assets/vars.scss
new file mode 100644
index 0000000..7493c8a
--- /dev/null
+++ b/assets/src/assets/vars.scss
@@ -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;
diff --git a/assets/src/assets/vendor.scss b/assets/src/assets/vendor.scss
new file mode 100644
index 0000000..a2c309b
--- /dev/null
+++ b/assets/src/assets/vendor.scss
@@ -0,0 +1,33 @@
+@import 'v-calendar/style.css';
+
+// ---- bulma
+$body-color: #000;
+$title-color: #000;
+
+
+@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";
diff --git a/assets/src/components/ACarousel.vue b/assets/src/components/ACarousel.vue
new file mode 100644
index 0000000..5ee0611
--- /dev/null
+++ b/assets/src/components/ACarousel.vue
@@ -0,0 +1,242 @@
+
+
+
+
+
+
+
+
+
diff --git a/assets/src/components/ADropdown.vue b/assets/src/components/ADropdown.vue
new file mode 100644
index 0000000..179c830
--- /dev/null
+++ b/assets/src/components/ADropdown.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+ {{ label }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/src/components/AEpisode.vue b/assets/src/components/AEpisode.vue
index abeec35..bc14994 100644
--- a/assets/src/components/AEpisode.vue
+++ b/assets/src/components/AEpisode.vue
@@ -1,7 +1,5 @@
-
-
-
+
diff --git a/assets/src/components/index.js b/assets/src/components/index.js
index cebbd78..5467c95 100644
--- a/assets/src/components/index.js
+++ b/assets/src/components/index.js
@@ -1,4 +1,6 @@
import AAutocomplete from './AAutocomplete.vue'
+import ACarousel from './ACarousel.vue'
+import ADropdown from "./ADropdown.vue"
import AEpisode from './AEpisode.vue'
import AList from './AList.vue'
import APage from './APage.vue'
@@ -7,6 +9,7 @@ import APlaylist from './APlaylist.vue'
import APlaylistEditor from './APlaylistEditor.vue'
import AProgress from './AProgress.vue'
import ASoundItem from './ASoundItem.vue'
+import ASwitch from './ASwitch.vue'
import AStatistics from './AStatistics.vue'
import AStreamer from './AStreamer.vue'
@@ -14,8 +17,8 @@ import AStreamer from './AStreamer.vue'
* Core components
*/
export const base = {
- AAutocomplete, AEpisode, AList, APage, APlayer, APlaylist,
- AProgress, ASoundItem,
+ AAutocomplete, ACarousel, ADropdown, AEpisode, AList, APage, APlayer, APlaylist,
+ AProgress, ASoundItem, ASwitch
}
export default base
diff --git a/assets/src/index.js b/assets/src/index.js
index 0bb8482..b7f6cf5 100644
--- a/assets/src/index.js
+++ b/assets/src/index.js
@@ -8,22 +8,22 @@ import '@fortawesome/fontawesome-free/css/all.min.css';
//-- aircox
import App, {PlayerApp} from './app'
-import Builder from './appBuilder'
+import VueLoader from './vueLoader'
import Sound from './sound'
import {Set} from './model'
-import './assets/styles.scss'
+import './assets/common.scss'
window.aircox = {
// main application
- builder: new Builder(App),
- get app() { return this.builder.app },
+ loader: null,
+ get app() { return this.loader.app },
// player application
- playerBuilder: new Builder(PlayerApp),
- get playerApp() { return this.playerBuilder && this.playerBuilder.app },
- get player() { return this.playerBuilder.vm && this.playerBuilder.vm.$refs.player },
+ playerLoader: null,
+ get playerApp() { return this.playerLoader && this.playerLoader.app },
+ get player() { return this.playerLoader.vm && this.playerLoader.vm.$refs.player },
Set, Sound,
@@ -31,27 +31,33 @@ window.aircox = {
/**
* Initialize main application and player.
*/
- init(props=null, {config=null, builder=null, initBuilder=true,
- initPlayer=true, hotReload=false, el=null}={})
+ init(props=null, {hotReload=false, el=null,
+ config=null, playerConfig=null,
+ initApp=true, initPlayer=true,
+ loader=null, playerLoader=null}={})
{
if(initPlayer) {
- let playerBuilder = this.playerBuilder
- playerBuilder.mount()
+ playerConfig = playerConfig || PlayerApp
+ playerLoader = playerLoader || new VueLoader(playerConfig)
+ playerLoader.enable(false)
+ this.playerLoader = playerLoader
+
+ document.addEventListener("keyup", e => this.onKeyPress(e), false)
}
- if(initBuilder) {
- builder = builder || this.builder
- this.builder = builder
- if(config || window.App)
- builder.config = config || window.App
- if(el)
- builder.config.el = el
+ 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
+ }
+ },
- builder.title = document.title
- builder.mount({props})
-
- if(hotReload)
- builder.enableHotReload(hotReload)
+ onKeyPress(event) {
+ if(event.key == " ") {
+ this.player.togglePlay()
+ event.stopPropagation()
}
},
@@ -68,5 +74,10 @@ window.aircox = {
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)
}
}
diff --git a/assets/src/pageLoad.js b/assets/src/pageLoad.js
new file mode 100644
index 0000000..e41c0a1
--- /dev/null
+++ b/assets/src/pageLoad.js
@@ -0,0 +1,174 @@
+
+/**
+ * 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)
+ }
+
+ // --- 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'))
+ 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.historySave(url))
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ statePopped(event) {
+ const state = event.state
+ if(state && state.content)
+ this.mount({ content: state.content, title: state.title });
+ }
+}
diff --git a/assets/src/core.js b/assets/src/public.js
similarity index 73%
rename from assets/src/core.js
rename to assets/src/public.js
index dbcaea4..c197f63 100644
--- a/assets/src/core.js
+++ b/assets/src/public.js
@@ -1,3 +1,4 @@
+import "./assets/public.scss"
import './index.js'
import App from './app.js'
diff --git a/assets/src/vueLoader.js b/assets/src/vueLoader.js
new file mode 100644
index 0000000..0556c17
--- /dev/null
+++ b/assets/src/vueLoader.js
@@ -0,0 +1,47 @@
+import {createApp} from 'vue'
+
+import PageLoad from './pageLoad'
+
+
+/**
+ * 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)
+ }
+
+ 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() }
+}
diff --git a/assets/vue.config.js b/assets/vue.config.js
index a44bd6e..c295d83 100644
--- a/assets/vue.config.js
+++ b/assets/vue.config.js
@@ -16,7 +16,7 @@ module.exports = defineConfig({
},
pages: {
- core: { entry: 'src/core.js', },
+ public: { entry: 'src/public.js' },
admin: { entry: 'src/admin.js' },
}
})
diff --git a/instance/settings/base.py b/instance/settings/base.py
index d18dbd6..30b5a6a 100755
--- a/instance/settings/base.py
+++ b/instance/settings/base.py
@@ -183,6 +183,7 @@ THUMBNAIL_PROCESSORS = (
# Enabled applications
INSTALLED_APPS = (
+ "radiocampus",
"aircox.apps.AircoxConfig",
"aircox.apps.AircoxAdminConfig",
"aircox_streamer.apps.AircoxStreamerConfig",
diff --git a/notes.md b/notes.md
index d9dcba9..9553f88 100755
--- a/notes.md
+++ b/notes.md
@@ -1,3 +1,21 @@
+# TODO
+- card: url
+- page header:
+ - content inline
+ - responsive
+
+- remove vue-carousel
+- statistics & monitor
+
+
+# Proposals
+- diffusion list view for a program + link on program page view
+- add podcast list to playlist
+- pause on "space" key
+
+
+##############
+
This file is used as a reminder, can be used as crappy documentation too.
- player
diff --git a/radiocampus/__init__.py b/radiocampus/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/radiocampus/apps.py b/radiocampus/apps.py
new file mode 100644
index 0000000..0d4399e
--- /dev/null
+++ b/radiocampus/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class RadiocampusConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "radiocampus"
diff --git a/radiocampus/migrations/__init__.py b/radiocampus/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/radiocampus/static/radiocampus/fonts/CampusGroteskv11-Regular.otf b/radiocampus/static/radiocampus/fonts/CampusGroteskv11-Regular.otf
new file mode 100644
index 0000000..c59c345
Binary files /dev/null and b/radiocampus/static/radiocampus/fonts/CampusGroteskv11-Regular.otf differ
diff --git a/radiocampus/static/radiocampus/fonts/CampusGroteskv12-Regular.otf b/radiocampus/static/radiocampus/fonts/CampusGroteskv12-Regular.otf
new file mode 100644
index 0000000..cf6ce84
Binary files /dev/null and b/radiocampus/static/radiocampus/fonts/CampusGroteskv12-Regular.otf differ
diff --git a/radiocampus/static/radiocampus/fonts/CampusGroteskv8-Regular.otf b/radiocampus/static/radiocampus/fonts/CampusGroteskv8-Regular.otf
new file mode 100644
index 0000000..8500aa3
Binary files /dev/null and b/radiocampus/static/radiocampus/fonts/CampusGroteskv8-Regular.otf differ
diff --git a/radiocampus/templates/aircox/base.html b/radiocampus/templates/aircox/base.html
new file mode 100644
index 0000000..bff270d
--- /dev/null
+++ b/radiocampus/templates/aircox/base.html
@@ -0,0 +1,23 @@
+{% extends "aircox/base.html" %}
+{% load static %}
+
+{% block head_extra %}
+{{ block.super }}
+
+{% endblock %}
+
+
+{% block header-container %}
+{% if not page.attach_to %}
+{{ block.super }}
+{% endif %}
+{% endblock %}
|