radiocampus: style update
							
								
								
									
										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))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										67
									
								
								radiocampus/static/radiocampus/backgroundLoad.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,67 @@
 | 
				
			|||||||
 | 
					// 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', "url(/static/radiocampus/backgrounds/campus-degrade.png) repeat-x center top scroll");
 | 
				
			||||||
 | 
					backgrounds.set('/', "url(/static/radiocampus/backgrounds/home.jpg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					//backgrounds.set('default', "linear-gradient(rgba(115, 142, 242, 0.9), white)");
 | 
				
			||||||
 | 
					//backgrounds.set('default', "white");
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					backgrounds.set('default', "linear-gradient(#738ef2, white)");
 | 
				
			||||||
 | 
					backgrounds.set('/', "url(/static/radiocampus/backgrounds/photo-04-20.jpg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					backgrounds.set('/publications/', "url(/static/radiocampus/backgrounds/photo-04-20.jpg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					backgrounds.set('/pages/a-propos/', "white");
 | 
				
			||||||
 | 
					backgrounds.set('/emissions/', "url(/static/radiocampus/backgrounds/08.jpeg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					backgrounds.set('/emissions/c/musicale/', "url(/static/radiocampus/backgrounds/08.jpeg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					backgrounds.set('/emissions/c/communautaire/', "url(/static/radiocampus/backgrounds/08.jpeg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					backgrounds.set('/emissions/c/magazine/', "url(/static/radiocampus/backgrounds/08.jpeg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					backgrounds.set('/emissions/c/creation/', "url(/static/radiocampus/backgrounds/08.jpeg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					backgrounds.set('/emissions/c/agenda-culturel/', "url(/static/radiocampus/backgrounds/08.jpeg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					backgrounds.set('/grille/', "url(/static/radiocampus/backgrounds/photo-04-19.jpg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					backgrounds.set('/podcasts/', "url(/static/radiocampus/backgrounds/09.jpeg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					backgrounds.set('/podcasts/c/musicale/', "url(/static/radiocampus/backgrounds/09.jpeg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					backgrounds.set('/podcasts/c/communautaire/', "url(/static/radiocampus/backgrounds/09.jpeg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					backgrounds.set('/podcasts/c/magazine/', "url(/static/radiocampus/backgrounds/09.jpeg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					backgrounds.set('/podcasts/c/creation/', "url(/static/radiocampus/backgrounds/09.jpeg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					backgrounds.set('/podcasts/c/agenda-culturel/', "url(/static/radiocampus/backgrounds/09.jpeg) no-repeat center center fixed");
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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");
 | 
				
			||||||
 | 
					        let target = document.body;
 | 
				
			||||||
 | 
					        // let target = document.getElementsByClassName("page")[0];
 | 
				
			||||||
 | 
					        //let target = document.getElementById("background");
 | 
				
			||||||
 | 
					        target.style.background = background;
 | 
				
			||||||
 | 
					        if (this.path == '/') {
 | 
				
			||||||
 | 
					            target.style.backgroundSize = "cover";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const styleEl = document.createElement("style");
 | 
				
			||||||
 | 
					            document.head.appendChild(styleEl);
 | 
				
			||||||
 | 
					            const styleSheet = styleEl.sheet;
 | 
				
			||||||
 | 
					            styleSheet.insertRule('.preview .headings a { color: white; }', styleSheet.cssRules.length);
 | 
				
			||||||
 | 
					            styleSheet.insertRule('.preview .headings a:hover { color: #ffcdee !important; }', styleSheet.cssRules.length);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            const styleEl = document.createElement("style");
 | 
				
			||||||
 | 
					            document.head.appendChild(styleEl);
 | 
				
			||||||
 | 
					            const styleSheet = styleEl.sheet;
 | 
				
			||||||
 | 
					            styleSheet.insertRule('.preview .headings a { color: #75124e; }', styleSheet.cssRules.length);
 | 
				
			||||||
 | 
					            styleSheet.insertRule('.preview .headings a:hover { color: #75124e !important; }', styleSheet.cssRules.length);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					window.onload = function() {
 | 
				
			||||||
 | 
					    let backgroundLoad = new BackgroundLoad();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								radiocampus/static/radiocampus/backgrounds/08.jpeg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.9 MiB  | 
							
								
								
									
										
											BIN
										
									
								
								radiocampus/static/radiocampus/backgrounds/09.jpeg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.7 MiB  | 
							
								
								
									
										
											BIN
										
									
								
								radiocampus/static/radiocampus/backgrounds/campus-degrade.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								radiocampus/static/radiocampus/backgrounds/home.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.6 MiB  | 
							
								
								
									
										
											BIN
										
									
								
								radiocampus/static/radiocampus/backgrounds/photo-04-19.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 448 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								radiocampus/static/radiocampus/backgrounds/photo-04-20.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.8 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  | 
							
								
								
									
										258
									
								
								radiocampus/static/radiocampus/radiocampus.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,258 @@
 | 
				
			|||||||
 | 
					: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; /*fff667;*/
 | 
				
			||||||
 | 
					    --a-sound-bg: #f6ed80;
 | 
				
			||||||
 | 
					    --body-bg: unset;
 | 
				
			||||||
 | 
					    --break-color: transparent;
 | 
				
			||||||
 | 
					    --button-bg: #F4F88D;
 | 
				
			||||||
 | 
					    --button-fg: #222;
 | 
				
			||||||
 | 
					    --button-hv-bg: #F4F88D;
 | 
				
			||||||
 | 
					    --button-hv-fg: #1d3cab;
 | 
				
			||||||
 | 
					    --button-active-fg: white;
 | 
				
			||||||
 | 
					    --button-active-bg: #eef289;
 | 
				
			||||||
 | 
					    --heading-font-family: "campus_heading";
 | 
				
			||||||
 | 
					    --heading-link-hv-fg: #aa217b;
 | 
				
			||||||
 | 
					    --heading-hg-fg: #e65fb1;
 | 
				
			||||||
 | 
					    --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; /* #fdffba; */
 | 
				
			||||||
 | 
					    --text-color: #75124e; /*#ed43ab;*/
 | 
				
			||||||
 | 
					    --text-color-light: #bbb;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					    font-family: 'campus_heading';
 | 
				
			||||||
 | 
					    src: url("/static/radiocampus/fonts/CampusGroteskv23-Regular.otf");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
					    color: #222;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#grandlogo {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    background: url('/static/radiocampus/images/grand-logo.png') no-repeat scroll top center;
 | 
				
			||||||
 | 
					    background-size: contain;
 | 
				
			||||||
 | 
					    height: 107px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.a-player-bar {
 | 
				
			||||||
 | 
					    border-top: 1px solid #555;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.a-player .button, .a-player-bar-content {
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#player .a-sound-item .label {
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.a-switch-nav span {
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.a-switch-nav span:hover {
 | 
				
			||||||
 | 
					    color: #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button, a.button, button.button {
 | 
				
			||||||
 | 
					  border: var(--link-fg) 1px solid;
 | 
				
			||||||
 | 
					  border-radius: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav.primary .nav-brand {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					.navs {
 | 
				
			||||||
 | 
					    background-color: #738ef2;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav.secondary .nav-item {
 | 
				
			||||||
 | 
					    color: #7E6B64 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-item:hover {
 | 
				
			||||||
 | 
					    opacity: 0.8;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.page section.container {
 | 
				
			||||||
 | 
					    margin-top: 0;
 | 
				
			||||||
 | 
					    padding-top: 0.6rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media screen and (max-width: 1024px) {
 | 
				
			||||||
 | 
					  .nav.primary {
 | 
				
			||||||
 | 
					      background-color: #738ef2;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .page {
 | 
				
			||||||
 | 
					      margin-top: 0;
 | 
				
			||||||
 | 
					      padding-top: var(--nav-primary-height);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .nav.primary .nav-brand {
 | 
				
			||||||
 | 
					          display: inline-block;
 | 
				
			||||||
 | 
					          background-color: #738ef2;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .dropdown-trigger .icon, .icon.bullet {
 | 
				
			||||||
 | 
					      color: white;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .dropdown.is-right .dropdown-menu {
 | 
				
			||||||
 | 
					      left: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #grandlogo {
 | 
				
			||||||
 | 
					        display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .nav .nav-menu {
 | 
				
			||||||
 | 
					        background-color: #738ef2;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .nav .nav-item {
 | 
				
			||||||
 | 
					        color: white !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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					.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{
 | 
				
			||||||
 | 
					    color: #1d3cab !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.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;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
@ -3,18 +3,20 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block head %}
 | 
					{% block head %}
 | 
				
			||||||
{{ block.super }}
 | 
					{{ block.super }}
 | 
				
			||||||
<style>
 | 
					<link rel='stylesheet' href='/static/radiocampus/radiocampus.css' type='text/css' media='all' />
 | 
				
			||||||
:root {
 | 
					<script src="{% static "radiocampus/backgroundLoad.js" %}"></script>
 | 
				
			||||||
    --heading-font-family: "campus_heading";
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@font-face {
 | 
					 | 
				
			||||||
    font-family: 'campus_heading';
 | 
					 | 
				
			||||||
    src: url('{% static "radiocampus/fonts/CampusGroteskv12-Regular.otf" %}');
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block nav %}
 | 
				
			||||||
 | 
					<a href="/"><div id="grandlogo"></div></a>
 | 
				
			||||||
 | 
					{{ block.super }}
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block main-container %}
 | 
				
			||||||
 | 
					<div id="background">
 | 
				
			||||||
 | 
					{{ block.super }}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block header-container %}
 | 
					{% block header-container %}
 | 
				
			||||||
{% if not page.attach_to %}
 | 
					{% if not page.attach_to %}
 | 
				
			||||||
 | 
				
			|||||||