upgrade vue and assets
This commit is contained in:
		@ -1,25 +0,0 @@
 | 
			
		||||
import AAutocomplete from './autocomplete'
 | 
			
		||||
import AEpisode from './episode'
 | 
			
		||||
import APlayer from './player'
 | 
			
		||||
import APlaylist from './playlist'
 | 
			
		||||
import ASoundItem from './soundItem'
 | 
			
		||||
 | 
			
		||||
const App = {
 | 
			
		||||
    el: '#app',
 | 
			
		||||
    delimiters: ['[[', ']]'],
 | 
			
		||||
 | 
			
		||||
    computed: {
 | 
			
		||||
        player() { return window.aircox.player; },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    components: {AAutocomplete, AEpisode, APlaylist, ASoundItem},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const PlayerApp = {
 | 
			
		||||
    el: '#player',
 | 
			
		||||
    components: {APlayer},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default App
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,137 +0,0 @@
 | 
			
		||||
import {createApp} from 'vue'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Utility class used to handle Vue applications. It provides way to load
 | 
			
		||||
 * remote application and update history.
 | 
			
		||||
 */
 | 
			
		||||
export default class Builder {
 | 
			
		||||
    constructor(config={}) {
 | 
			
		||||
        this.config = config
 | 
			
		||||
        this.title = null
 | 
			
		||||
        this.app = null
 | 
			
		||||
        this.vm = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch app from remote and mount application.
 | 
			
		||||
     */
 | 
			
		||||
    fetch(url, {el='app', ...options}={}) {
 | 
			
		||||
        return fetch(url, options).then(response => response.text())
 | 
			
		||||
            .then(content => {
 | 
			
		||||
                let doc = new DOMParser().parseFromString(content, 'text/html')
 | 
			
		||||
                let app = doc.getElementById('app')
 | 
			
		||||
                content = app ? app.innerHTML : content
 | 
			
		||||
                return this.mount({content, title: doc.title, reset:true, url })
 | 
			
		||||
            })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Mount application, using `create_app` if required.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {String} options.content: replace app container content with it
 | 
			
		||||
     * @param {String} options.title: set DOM document title.
 | 
			
		||||
     * @param {String} [options.el=this.config.el]: mount application on this element (querySelector argument)
 | 
			
		||||
     * @param {Boolean} [reset=False]: if True, force application recreation.
 | 
			
		||||
     * @return `app.mount`'s result.
 | 
			
		||||
     */
 | 
			
		||||
    mount({content=null, title=null, el=null, reset=false, props=null}={}) {
 | 
			
		||||
        try {
 | 
			
		||||
            this.unmount()
 | 
			
		||||
            
 | 
			
		||||
            let config = this.config
 | 
			
		||||
            if(el === null)
 | 
			
		||||
                el = config.el
 | 
			
		||||
            if(reset || !this.app)
 | 
			
		||||
                this.app = this.createApp({title,content,el,...config}, props)
 | 
			
		||||
 | 
			
		||||
            this.vm = this.app.mount(el)
 | 
			
		||||
            window.scroll(0, 0)
 | 
			
		||||
            return this.vm
 | 
			
		||||
        } catch(error) {
 | 
			
		||||
            this.unmount()
 | 
			
		||||
            throw error
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    createApp({el, title=null, content=null, ...config}, props) {
 | 
			
		||||
        const container = document.querySelector(el)
 | 
			
		||||
        if(!container)
 | 
			
		||||
            throw `Error: can't get element ${el}`
 | 
			
		||||
        if(content)
 | 
			
		||||
            container.innerHTML = content
 | 
			
		||||
        if(title)
 | 
			
		||||
            document.title = title
 | 
			
		||||
        return createApp(config, props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    unmount() {
 | 
			
		||||
        this.app && this.app.unmount()
 | 
			
		||||
        this.app = null
 | 
			
		||||
        this.vm = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Enable hot reload: catch page change in order to fetch them and
 | 
			
		||||
     * load page without actually leaving current one.
 | 
			
		||||
     */
 | 
			
		||||
    enableHotReload(node=null, historySave=true) {
 | 
			
		||||
        if(historySave)
 | 
			
		||||
            this.historySave(document.location, true)
 | 
			
		||||
        node.addEventListener('click', event => this._onPageChange(event), true)
 | 
			
		||||
        node.addEventListener('submit', event => this._onPageChange(event), true)
 | 
			
		||||
        node.addEventListener('popstate', event => this._onPopState(event), true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _onPageChange(event) {
 | 
			
		||||
        let submit = event.type == 'submit';
 | 
			
		||||
        let target = submit || event.target.tagName == 'A'
 | 
			
		||||
                        ? event.target : event.target.closest('a');
 | 
			
		||||
        if(!target || target.hasAttribute('target'))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        let url = submit ? target.getAttribute('action') || ''
 | 
			
		||||
                         : target.getAttribute('href');
 | 
			
		||||
        if(url===null || !(url === '' || url.startsWith('/') || url.startsWith('?')))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        let options = {};
 | 
			
		||||
        if(submit) {
 | 
			
		||||
            let formData = new FormData(event.target);
 | 
			
		||||
            if(target.method == 'get')
 | 
			
		||||
                url += '?' + (new URLSearchParams(formData)).toString();
 | 
			
		||||
            else
 | 
			
		||||
                options = {...options, method: target.method, body: formData}
 | 
			
		||||
        }
 | 
			
		||||
        this.fetch(url, options).then(_ => this.historySave(url))
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _onPopState(event) {
 | 
			
		||||
        if(event.state && event.state.content)
 | 
			
		||||
            // document.title = this.title;
 | 
			
		||||
            this.historyLoad(event.state);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Save application state into browser history
 | 
			
		||||
    historySave(url,replace=false) {
 | 
			
		||||
        const el = document.querySelector(this.config.el)
 | 
			
		||||
        const state = {
 | 
			
		||||
            content: el.innerHTML,
 | 
			
		||||
            title: document.title,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if(replace)
 | 
			
		||||
            history.replaceState(state, '', url)
 | 
			
		||||
        else
 | 
			
		||||
            history.pushState(state, '', url)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Load application from browser history's state
 | 
			
		||||
    historyLoad(state) {
 | 
			
		||||
        return this.mount({ content: state.content, title: state.title })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,76 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="control">
 | 
			
		||||
        <datalist :id="listId">
 | 
			
		||||
            <template v-for="item in items" :key="item.path">
 | 
			
		||||
                <option :value="item[field]"></option>
 | 
			
		||||
            </template>
 | 
			
		||||
        </datalist>
 | 
			
		||||
        <input type="text" :name="name" :placeholder="placeholder"
 | 
			
		||||
            :list="listId" @keyup="onKeyUp"/>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
// import debounce from 'lodash/debounce'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    props: {
 | 
			
		||||
        url: String,
 | 
			
		||||
        model: Function,
 | 
			
		||||
        placeholder: String,
 | 
			
		||||
        name: String,
 | 
			
		||||
        field: String,
 | 
			
		||||
        valueField: {type: String, default: 'id'},
 | 
			
		||||
        count: {type: Number, count: 10},
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            value: '',
 | 
			
		||||
            items: [],
 | 
			
		||||
            selected: null,
 | 
			
		||||
            isFetching: false,
 | 
			
		||||
            listId: `autocomplete-${ Math.random() }`.replace('.',''),
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        select(option, value=null) {
 | 
			
		||||
            if(!option && value !== null)
 | 
			
		||||
                option = this.items.find(item => item[this.field] == value)
 | 
			
		||||
 | 
			
		||||
            this.selected = option
 | 
			
		||||
            this.$emit('select', option)
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        onKeyUp: function(event) {
 | 
			
		||||
            const value = event.target.value
 | 
			
		||||
            if(value === this.value)
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            if(value !== undefined && value !== null)
 | 
			
		||||
                this.value = value
 | 
			
		||||
                
 | 
			
		||||
            if(!value)
 | 
			
		||||
                return this.select(null)
 | 
			
		||||
 | 
			
		||||
            this.fetch(value)
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        fetch: function(query) {
 | 
			
		||||
            if(!query || this.isFetching)
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            this.isFetching = true
 | 
			
		||||
            return this.model.fetch(this.url.replace('${query}', query), {many:true})
 | 
			
		||||
                .then(items => { this.items = items || []
 | 
			
		||||
                                 this.isFetching = false
 | 
			
		||||
                                 this.select(null, query)
 | 
			
		||||
                                 return items },
 | 
			
		||||
                      data => {this.isFetching = false; Promise.reject(data)})
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -1,23 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <slot :page="page" :podcasts="podcasts"></slot>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import {Set} from './model';
 | 
			
		||||
import Sound from './sound';
 | 
			
		||||
import Page from './page';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    extends: Page,
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            podcasts: new Set(Sound, {items:this.page.podcasts}),
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,60 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * This module includes code available for both the public website and
 | 
			
		||||
 * administration interface)
 | 
			
		||||
 */
 | 
			
		||||
//-- vendor
 | 
			
		||||
import '@fortawesome/fontawesome-free/css/all.min.css'
 | 
			
		||||
import '@fortawesome/fontawesome-free/css/fontawesome.min.css'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
//-- aircox
 | 
			
		||||
import App, {PlayerApp} from './app'
 | 
			
		||||
import Builder from './appBuilder'
 | 
			
		||||
import Sound from './sound'
 | 
			
		||||
import {Set} from './model'
 | 
			
		||||
 | 
			
		||||
import './styles.scss'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
window.aircox = {
 | 
			
		||||
    // main application
 | 
			
		||||
    builder: new Builder(App),
 | 
			
		||||
    get app() { return this.builder.app  },
 | 
			
		||||
 | 
			
		||||
    // player application
 | 
			
		||||
    playerBuilder: new Builder(PlayerApp),
 | 
			
		||||
    get playerApp() { return this.playerBuilder && this.playerBuilder.app },
 | 
			
		||||
    get player() { return this.playerBuilder.vm && this.playerBuilder.vm.$refs.player },
 | 
			
		||||
 | 
			
		||||
    Set, Sound,
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize main application and player.
 | 
			
		||||
     */
 | 
			
		||||
    init(props=null, {config=null, builder=null, initPlayer=true}={}) {
 | 
			
		||||
        builder = builder || this.builder
 | 
			
		||||
        this.builder = builder
 | 
			
		||||
        if(config)
 | 
			
		||||
            builder.config = config
 | 
			
		||||
        builder.title = document.title
 | 
			
		||||
        builder.mount({props})
 | 
			
		||||
 | 
			
		||||
        if(initPlayer) {
 | 
			
		||||
            let playerBuilder = this.playerBuilder
 | 
			
		||||
            playerBuilder.mount()
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
window.addEventListener('load', e => {
 | 
			
		||||
    const [app, player] = [aircox.builder, aircox.playerBuilder]
 | 
			
		||||
    app.title = document.title
 | 
			
		||||
    app.mount()
 | 
			
		||||
    app.enableHotReload(window)
 | 
			
		||||
 | 
			
		||||
    player.mount()
 | 
			
		||||
})
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
@ -1,75 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <slot name="header"></slot>
 | 
			
		||||
        <ul :class="listClass">
 | 
			
		||||
            <template v-for="(item,index) in items">
 | 
			
		||||
                <li :class="itemClass" @click="select(index)">
 | 
			
		||||
                    <slot name="item" :selected="index == selectedIndex" :set="set" :index="index" :item="item"></slot>
 | 
			
		||||
                </li>
 | 
			
		||||
            </template>
 | 
			
		||||
        </ul>
 | 
			
		||||
        <slot name="footer"></slot>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            selectedIndex: this.defaultIndex,
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    props: {
 | 
			
		||||
        listClass: String,
 | 
			
		||||
        itemClass: String,
 | 
			
		||||
        defaultIndex: { type: Number, default: -1},
 | 
			
		||||
        set: Object,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    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) },
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Add items to list, return index of the first provided item.
 | 
			
		||||
         */
 | 
			
		||||
        push(item, ...items) {
 | 
			
		||||
            let index = this.set.push(item);
 | 
			
		||||
            for(var item of items)
 | 
			
		||||
                this.set.push(item);
 | 
			
		||||
            return index;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        remove(index, select=False) {
 | 
			
		||||
            this.set.remove(index);
 | 
			
		||||
            if(index < this.selectedIndex)
 | 
			
		||||
                this.selectedIndex--;
 | 
			
		||||
            if(select && this.selectedIndex == index)
 | 
			
		||||
                this.select(index)
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
@ -1,50 +0,0 @@
 | 
			
		||||
import {setEcoTimeout} from 'public/utils';
 | 
			
		||||
import Model from './model';
 | 
			
		||||
 | 
			
		||||
export default class Live {
 | 
			
		||||
    constructor({url,timeout=10,src=""}={}) {
 | 
			
		||||
        this.url = url;
 | 
			
		||||
        this.timeout = timeout;
 | 
			
		||||
        this.src = src;
 | 
			
		||||
 | 
			
		||||
        this.promise = null;
 | 
			
		||||
        this.items = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get current() {
 | 
			
		||||
        let item = this.items && this.items[this.items.length-1];
 | 
			
		||||
        if(item)
 | 
			
		||||
            item.src = this.src;
 | 
			
		||||
        return item ? new Model(item) : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //-- data refreshing
 | 
			
		||||
    drop() {
 | 
			
		||||
        this.promise = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fetch() {
 | 
			
		||||
        const promise = fetch(this.url).then(response =>
 | 
			
		||||
            response.ok ? response.json()
 | 
			
		||||
                        : Promise.reject(response)
 | 
			
		||||
        ).then(data => {
 | 
			
		||||
            this.items = data;
 | 
			
		||||
            return this.items
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        this.promise = promise;
 | 
			
		||||
        return promise;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    refresh() {
 | 
			
		||||
        const promise = this.fetch();
 | 
			
		||||
        promise.then(data => {
 | 
			
		||||
            if(promise != this.promise)
 | 
			
		||||
                return [];
 | 
			
		||||
 | 
			
		||||
            setEcoTimeout(() => this.refresh(), this.timeout*1000)
 | 
			
		||||
        })
 | 
			
		||||
        return promise
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								assets/public/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/public/logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 7.1 KiB  | 
@ -1,217 +0,0 @@
 | 
			
		||||
 | 
			
		||||
function getCookie(name) {
 | 
			
		||||
    if(document.cookie && document.cookie !== '') {
 | 
			
		||||
        const cookie = document.cookie.split(';')
 | 
			
		||||
                               .find(c => c.trim().startsWith(name + '='))
 | 
			
		||||
        return cookie ? decodeURIComponent(cookie.split('=')[1]) : null;
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var csrfToken = null;
 | 
			
		||||
 | 
			
		||||
export function getCsrf() {
 | 
			
		||||
    if(csrfToken === null)
 | 
			
		||||
        csrfToken = getCookie('csrftoken')
 | 
			
		||||
    return csrfToken;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// TODO: prevent duplicate simple fetch
 | 
			
		||||
export default class Model {
 | 
			
		||||
    constructor(data, {url=null}={}) {
 | 
			
		||||
        this.url = url || data.url_;
 | 
			
		||||
        this.commit(data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get instance id from its data
 | 
			
		||||
     */
 | 
			
		||||
    static getId(data) {
 | 
			
		||||
        return data.id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return fetch options
 | 
			
		||||
     */
 | 
			
		||||
    static getOptions(options) {
 | 
			
		||||
        return {
 | 
			
		||||
            headers: {
 | 
			
		||||
                'Content-Type': 'application/json',
 | 
			
		||||
                'Accept': 'application/json',
 | 
			
		||||
                'X-CSRFToken': getCsrf(),
 | 
			
		||||
            },
 | 
			
		||||
            ...options,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static fromList(items, args=null) {
 | 
			
		||||
        return items ? items.map(d => new this(d, args)) : []
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update instance's data with provided data. Return None
 | 
			
		||||
     */
 | 
			
		||||
    commit(data) {
 | 
			
		||||
        this.id = this.constructor.getId(data);
 | 
			
		||||
        this.data = data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get length() { return this.items.length }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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})))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add item to set, return index.
 | 
			
		||||
     */
 | 
			
		||||
    push(item, {args={},save=true}={}) {
 | 
			
		||||
        item = item instanceof this.model ? item : new this.model(item, args);
 | 
			
		||||
        if(this.unique) {
 | 
			
		||||
            let index = this.findIndex(item);
 | 
			
		||||
            if(index > -1)
 | 
			
		||||
                return index;
 | 
			
		||||
        }
 | 
			
		||||
        if(this.max && this.items.length >= this.max)
 | 
			
		||||
            this.items.splice(0,this.items.length-this.max)
 | 
			
		||||
 | 
			
		||||
        this.items.push(item);
 | 
			
		||||
        save && this.save();
 | 
			
		||||
        return this.items.length-1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove item from set by index
 | 
			
		||||
     */
 | 
			
		||||
    remove(index, {save=true}={}) {
 | 
			
		||||
        this.items.splice(index,1);
 | 
			
		||||
        save && this.save();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Set[Symbol.iterator] = function () {
 | 
			
		||||
    return this.items[Symbol.iterator]();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,20 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <slot></slot>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
    data() {
 | 
			
		||||
        return {}
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    props: {
 | 
			
		||||
        page: Object,
 | 
			
		||||
        title: String,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,286 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="player">
 | 
			
		||||
        <div :class="['player-panels', panel ? 'is-open' : '']">
 | 
			
		||||
            <Playlist ref="pin" class="player-panel menu" v-show="panel == 'pin'"
 | 
			
		||||
                name="Pinned"
 | 
			
		||||
                :actions="['page']"
 | 
			
		||||
                :editable="true" :player="self" :set="sets.pin" @select="togglePlay('pin', $event.index)" 
 | 
			
		||||
                listClass="menu-list" itemClass="menu-item">
 | 
			
		||||
                <template v-slot:header="">
 | 
			
		||||
                    <p class="menu-label">
 | 
			
		||||
                        <span class="icon"><span class="fa fa-thumbtack"></span></span>
 | 
			
		||||
                        Pinned
 | 
			
		||||
                    </p>
 | 
			
		||||
                </template>
 | 
			
		||||
            </Playlist>
 | 
			
		||||
            <Playlist ref="queue" class="player-panel menu" v-show="panel == 'queue'"
 | 
			
		||||
                :actions="['page']"
 | 
			
		||||
                :editable="true" :player="self" :set="sets.queue" @select="togglePlay('queue', $event.index)" 
 | 
			
		||||
                listClass="menu-list" itemClass="menu-item">
 | 
			
		||||
                <template v-slot:header="">
 | 
			
		||||
                    <p class="menu-label">
 | 
			
		||||
                        <span class="icon"><span class="fa fa-list"></span></span>
 | 
			
		||||
                        Playlist
 | 
			
		||||
                    </p>
 | 
			
		||||
                </template>
 | 
			
		||||
            </Playlist>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="player-bar media">
 | 
			
		||||
            <div class="media-left">
 | 
			
		||||
                <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>
 | 
			
		||||
            <div class="media-left media-cover" v-if="current && current.data.cover">
 | 
			
		||||
                <img :src="current.data.cover" class="cover" />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="media-content">
 | 
			
		||||
                <slot name="content" :loaded='loaded' :live='live'></slot>
 | 
			
		||||
                <Progress v-if="loaded && duration" :value="currentTime" :max="this.duration"
 | 
			
		||||
                    :format="displayTime"
 | 
			
		||||
                    @select="audio.currentTime = $event"></Progress>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="media-right">
 | 
			
		||||
                <button class="button has-text-weight-bold" v-if="loaded" @click="play()">
 | 
			
		||||
                    <span class="icon is-size-6 has-text-danger">
 | 
			
		||||
                        <span class="fa fa-circle"></span>
 | 
			
		||||
                    </span>
 | 
			
		||||
                    <span>Live</span>
 | 
			
		||||
                </button>
 | 
			
		||||
                <button ref="pinPlaylistButton" :class="playlistButtonClass('pin')"
 | 
			
		||||
                        @click="togglePanel('pin')">
 | 
			
		||||
                    <span class="mr-2 is-size-6" v-if="sets.pin.length">
 | 
			
		||||
                        {{ sets.pin.length }}</span>
 | 
			
		||||
                    <span class="icon"><span class="fa fa-thumbtack"></span></span>
 | 
			
		||||
                </button>
 | 
			
		||||
                <button :class="playlistButtonClass('queue')"
 | 
			
		||||
                        @click="togglePanel('queue')">
 | 
			
		||||
                    <span class="mr-2 is-size-6" v-if="sets.queue.length">
 | 
			
		||||
                        {{ sets.queue.length }}</span>
 | 
			
		||||
                    <span class="icon"><span class="fa fa-list"></span></span>
 | 
			
		||||
                </button>
 | 
			
		||||
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import Vue, { ref } from 'vue';
 | 
			
		||||
import Live from './live';
 | 
			
		||||
import Playlist from './playlist';
 | 
			
		||||
import Progress from './progress';
 | 
			
		||||
import Sound from './sound';
 | 
			
		||||
import {Set} from './model';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export const State = {
 | 
			
		||||
    paused: 0,
 | 
			
		||||
    playing: 1,
 | 
			
		||||
    loading: 2,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    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', e => {
 | 
			
		||||
            this.currentTime = this.audio.currentTime;
 | 
			
		||||
        });
 | 
			
		||||
        audio.addEventListener('durationchange', e => {
 | 
			
		||||
            this.duration = Number.isFinite(this.audio.duration) ? this.audio.duration : null;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let live = this.liveArgs ? new Live(this.liveArgs) : null;
 | 
			
		||||
        live && live.refresh();
 | 
			
		||||
 | 
			
		||||
        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: {
 | 
			
		||||
                queue: Set.storeLoad(Sound, "playlist.queue", { max: 30, unique: true }),
 | 
			
		||||
                pin: Set.storeLoad(Sound, "player.pin", { max: 30, unique: true }),
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    props: {
 | 
			
		||||
        buttonTitle: String,
 | 
			
		||||
        liveArgs: 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] : 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 ? "is-info "
 | 
			
		||||
                          : this.playlistName == name ? 'is-primary '
 | 
			
		||||
                          : '') : '')
 | 
			
		||||
                + "button has-text-weight-bold";
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        /// 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].unselect();
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        /// Load a sound from playlist or live
 | 
			
		||||
        load(playlist=null, index=0) {
 | 
			
		||||
            let src = null;
 | 
			
		||||
 | 
			
		||||
            // from playlist
 | 
			
		||||
            if(playlist !== null) {
 | 
			
		||||
                let item = this.$refs[playlist].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.$refs[playlist].push(...items);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        /// Push and play items
 | 
			
		||||
        playItems(playlist, ...items) {
 | 
			
		||||
            let index = this.push(playlist, ...items);
 | 
			
		||||
            this.$refs[playlist].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) {
 | 
			
		||||
                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
 | 
			
		||||
        togglePin(item) {
 | 
			
		||||
            let index = this.sets.pin.findIndex(item);
 | 
			
		||||
            if(index > -1)
 | 
			
		||||
                this.sets.pin.remove(index);
 | 
			
		||||
            else {
 | 
			
		||||
                this.sets.pin.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();
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    components: { Playlist, Progress },
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,58 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <slot name="header"></slot>
 | 
			
		||||
        <ul :class="listClass">
 | 
			
		||||
            <li v-for="(item,index) in items" :class="itemClass" @click="!hasAction('play') && select(index)">
 | 
			
		||||
                <a :class="index == selectedIndex ? 'is-active' : ''">
 | 
			
		||||
                    <SoundItem
 | 
			
		||||
                        :data="item" :index="index" :player="player" :set="set"
 | 
			
		||||
                        @togglePlay="togglePlay(index)"
 | 
			
		||||
                        :actions="actions">
 | 
			
		||||
                        <template v-slot:actions="{loaded,set}">
 | 
			
		||||
                            <button class="button" v-if="editable" @click.stop="remove(index,true)">
 | 
			
		||||
                                <span class="icon is-small"><span class="fa fa-minus"></span></span>
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </template>
 | 
			
		||||
                    </SoundItem>
 | 
			
		||||
                </a>
 | 
			
		||||
            </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
        <slot name="footer"></slot>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
import List from './list';
 | 
			
		||||
import SoundItem from './soundItem';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    extends: List,
 | 
			
		||||
 | 
			
		||||
    props: {
 | 
			
		||||
        actions: Array,
 | 
			
		||||
        name: String,
 | 
			
		||||
        player: Object,
 | 
			
		||||
        editable: Boolean,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    computed: {
 | 
			
		||||
        self() { return this; }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    components: { List, SoundItem },
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
@ -1,67 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="media">
 | 
			
		||||
        <div class="media-left">
 | 
			
		||||
            <slot name="value" :value="valueDisplay" :max="max">{{ format(valueDisplay) }}</slot>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div ref="bar" class="media-content" @click.stop="onClick" @mouseleave.stop="onMouseMove"
 | 
			
		||||
                @mousemove.stop="onMouseMove">
 | 
			
		||||
            <div :class="progressClass" :style="progressStyle"> </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="media-right">
 | 
			
		||||
            <slot name="value" :value="valueDisplay" :max="max">{{ format(max) }}</slot>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            hoverValue: null,
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    props: {
 | 
			
		||||
        value: Number,
 | 
			
		||||
        max: Number,
 | 
			
		||||
        format: { type: Function, default: x => x },
 | 
			
		||||
        progressClass: { default: 'has-background-primary' },
 | 
			
		||||
        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>
 | 
			
		||||
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
import Model, {Set} from './model';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default class Sound extends Model {
 | 
			
		||||
    get name() { return this.data.name }
 | 
			
		||||
    get src() { return this.data.url }
 | 
			
		||||
 | 
			
		||||
    static getId(data) { return data.pk }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,61 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="media sound-item">
 | 
			
		||||
        <div class="media-left">
 | 
			
		||||
            <img class="cover is-tiny" :src="item.data.cover" v-if="item.data.cover">
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="media-left">
 | 
			
		||||
            <button class="button" @click.stop="$emit('togglePlay')">
 | 
			
		||||
                <div class="icon">
 | 
			
		||||
                    <span class="fa fa-pause" v-if="playing"></span>
 | 
			
		||||
                    <span class="fa fa-play" v-else></span>
 | 
			
		||||
                </div>
 | 
			
		||||
            </button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="media-content">
 | 
			
		||||
            <slot name="content" :player="player" :item="item" :loaded="loaded">
 | 
			
		||||
                <h4 class="title is-4">{{ name || item.name }}</h4>
 | 
			
		||||
                <a class="subtitle is-6 is-inline-block" v-if="hasAction('page') && item.data.page_url"
 | 
			
		||||
                    :href="item.data.page_url">
 | 
			
		||||
                    {{ item.data.page_title }}
 | 
			
		||||
                </a>
 | 
			
		||||
            </slot>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="media-right">
 | 
			
		||||
            <button class="button" v-if="player.sets.pin != $parent.set" @click.stop="player.togglePin(item)">
 | 
			
		||||
                <span class="icon is-small">
 | 
			
		||||
                    <span :class="(pinned ? '' : 'has-text-grey-light ') + 'fa fa-thumbtack'"></span>
 | 
			
		||||
                </span>
 | 
			
		||||
            </button>
 | 
			
		||||
            <slot name="actions" :player="player" :item="item" :loaded="loaded"></slot>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
import Model from './model';
 | 
			
		||||
import Sound from './sound';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    props: {
 | 
			
		||||
        data: {type: Object, default: x => {}},
 | 
			
		||||
        name: String,
 | 
			
		||||
        player: Object,
 | 
			
		||||
        page_url: String,
 | 
			
		||||
        actions: {type:Array, default: x => []},
 | 
			
		||||
        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>
 | 
			
		||||
@ -1,289 +0,0 @@
 | 
			
		||||
@charset "utf-8";
 | 
			
		||||
@import "~bulma/sass/utilities/_all.sass";
 | 
			
		||||
@import "~bulma/sass/components/dropdown.sass";
 | 
			
		||||
 | 
			
		||||
$body-background-color: $light;
 | 
			
		||||
 | 
			
		||||
@import "~buefy/src/scss/components/_autocomplete.scss";
 | 
			
		||||
@import "~bulma";
 | 
			
		||||
 | 
			
		||||
//-- helpers/modifiers
 | 
			
		||||
.is-fullwidth { width: 100%; }
 | 
			
		||||
.is-fixed-bottom {
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    margin-bottom: 0px;
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
}
 | 
			
		||||
.is-borderless { border: none; }
 | 
			
		||||
 | 
			
		||||
.has-text-nowrap {
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.has-background-transparent {
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.is-opacity-light {
 | 
			
		||||
    opacity: 0.7;
 | 
			
		||||
    &:hover {
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//-- navbar
 | 
			
		||||
.navbar + .container {
 | 
			
		||||
    margin-top: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar.has-shadow, .navbar.is-fixed-bottom.has-shadow {
 | 
			
		||||
    box-shadow: 0em 0em 1em rgba(0,0,0,0.1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a.navbar-item.is-active {
 | 
			
		||||
    border-bottom: 1px grey solid;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar {
 | 
			
		||||
    .navbar-dropdown {
 | 
			
		||||
        z-index: 2000;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .navbar-split {
 | 
			
		||||
        margin: 0.2em 0em;
 | 
			
		||||
        margin-right: 1em;
 | 
			
		||||
        padding-right: 1em;
 | 
			
		||||
        border-right: 1px $grey-light solid;
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    form {
 | 
			
		||||
        margin: 0em;
 | 
			
		||||
        padding: 0em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.toolbar {
 | 
			
		||||
        margin: 1em 0em;
 | 
			
		||||
        background-color: transparent;
 | 
			
		||||
        margin-bottom: 1em;
 | 
			
		||||
 | 
			
		||||
        .title {
 | 
			
		||||
            padding-right: 2em;
 | 
			
		||||
            margin-right: 1em;
 | 
			
		||||
            border-right: 1px $grey-light solid;
 | 
			
		||||
 | 
			
		||||
            font-size: $size-5;
 | 
			
		||||
            color: $text-light;
 | 
			
		||||
            font-weight: $weight-light;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//-- cards
 | 
			
		||||
.card {
 | 
			
		||||
    .title {
 | 
			
		||||
        a {
 | 
			
		||||
            color: $dark;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        padding: 0.2em;
 | 
			
		||||
        font-size: $size-5;
 | 
			
		||||
        font-weight: $weight-medium;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.is-primary {
 | 
			
		||||
        box-shadow: 0em 0em 0.5em $black
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-super-title {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    z-index: 1000;
 | 
			
		||||
    font-size: $size-6;
 | 
			
		||||
    font-weight: $weight-bold;
 | 
			
		||||
    padding: 0.2em;
 | 
			
		||||
    top: 1em;
 | 
			
		||||
    background-color: #ffffffc7;
 | 
			
		||||
    max-width: 90%;
 | 
			
		||||
 | 
			
		||||
    .fas {
 | 
			
		||||
        padding: 0.1em;
 | 
			
		||||
        font-size: 0.8em;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
//-- page
 | 
			
		||||
.page {
 | 
			
		||||
    & > .cover {
 | 
			
		||||
        float: right;
 | 
			
		||||
        max-width: 45%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .header {
 | 
			
		||||
        margin-bottom: 1.5em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .headline {
 | 
			
		||||
        font-size: 1.4em;
 | 
			
		||||
        padding: 0.2em 0em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    p { padding: 0.4em 0em; }
 | 
			
		||||
    hr { background-color: $grey-light; }
 | 
			
		||||
 | 
			
		||||
    .page-content {
 | 
			
		||||
        h1 { font-size: $size-1; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
 | 
			
		||||
        h2 { font-size: $size-3; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
 | 
			
		||||
        h3 { font-size: $size-4; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
 | 
			
		||||
        h4 { font-size: $size-5; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
 | 
			
		||||
        h5 { font-size: $size-6; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
 | 
			
		||||
        h6 { font-size: $size-6; margin-top:0.4em; margin-bottom:0.2em; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.media.item .headline {
 | 
			
		||||
    line-height: 1.2em;
 | 
			
		||||
    max-height: calc(1.2em * 3);
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
 | 
			
		||||
    & + .headline-overflow {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 2em;
 | 
			
		||||
        margin-top: -2em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & + .headline-overflow:before {
 | 
			
		||||
        content:'';
 | 
			
		||||
        width:100%;
 | 
			
		||||
        height:100%;
 | 
			
		||||
        position:absolute;
 | 
			
		||||
        left:0;
 | 
			
		||||
        bottom:0;
 | 
			
		||||
        background:linear-gradient(transparent 1em, $body-background-color);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
//-- player
 | 
			
		||||
.player {
 | 
			
		||||
    z-index: 10000;
 | 
			
		||||
    box-shadow: 0em 1.5em 2.5em rgba(0, 0, 0, 0.6);
 | 
			
		||||
 | 
			
		||||
    .player-panels {
 | 
			
		||||
        height: 0%;
 | 
			
		||||
        transition: height 3s;
 | 
			
		||||
    }
 | 
			
		||||
    .player-panels.is-open {
 | 
			
		||||
        height: auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .player-panel {
 | 
			
		||||
        margin: 0.4em;
 | 
			
		||||
        max-height: 80%;
 | 
			
		||||
        overflow-y: auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .progress {
 | 
			
		||||
        margin: 0em;
 | 
			
		||||
        padding: 0em;
 | 
			
		||||
        border-color: $info;
 | 
			
		||||
        border-style: 'solid';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .player-bar {
 | 
			
		||||
        border-top: 1px $grey-light solid;
 | 
			
		||||
 | 
			
		||||
        > .media-left:not(:last-child) {
 | 
			
		||||
            margin-right: 0em;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        > .media-cover {
 | 
			
		||||
            border-left: 1px black solid;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .cover {
 | 
			
		||||
            font-size: 1.5rem !important;
 | 
			
		||||
            height: 2.5em !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        > .media-content {
 | 
			
		||||
            padding-top: 0.4em;
 | 
			
		||||
            padding-left: 0.4em;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .button {
 | 
			
		||||
            font-size: 1.5rem !important;
 | 
			
		||||
            height: 2.5em;
 | 
			
		||||
            min-width: 2.5em;
 | 
			
		||||
            border-radius: 0px;
 | 
			
		||||
            transition: background-color 1s;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .title {
 | 
			
		||||
            margin: 0em;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//-- media
 | 
			
		||||
.media {
 | 
			
		||||
    .subtitle {
 | 
			
		||||
        margin-bottom: 0.4em;
 | 
			
		||||
    }
 | 
			
		||||
    .media-content .headline {
 | 
			
		||||
        font-size: 1em;
 | 
			
		||||
        font-weight: 400;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//-- general
 | 
			
		||||
body {
 | 
			
		||||
    background-color: $body-background-color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section > .toolbar {
 | 
			
		||||
    background-color: rgba(0,0,0,0.05);
 | 
			
		||||
    padding: 1em;
 | 
			
		||||
    margin-bottom: 1.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
main {
 | 
			
		||||
    .cover.is-small { width: 10em; }
 | 
			
		||||
    .cover.is-tiny { height: 2em; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
aside {
 | 
			
		||||
    & > section {
 | 
			
		||||
        margin-bottom: 2em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .cover {
 | 
			
		||||
        margin-bottom: 2em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .cover.is-small { width: 10em; }
 | 
			
		||||
    .cover.is-tiny { height: 2em; }
 | 
			
		||||
 | 
			
		||||
    .media .subtitle {
 | 
			
		||||
        font-size: 1em;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.sound-item {
 | 
			
		||||
    .cover { height: 5em; }
 | 
			
		||||
    .media-content a { padding: 0em; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.is-round, .sound-item .button {
 | 
			
		||||
    border: 1px $grey solid;
 | 
			
		||||
    border-radius: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export function setEcoTimeout(func, ...args) {
 | 
			
		||||
    return setTimeout((...args) => {
 | 
			
		||||
        !document.hidden && func(...args)
 | 
			
		||||
    }, ...args)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export function setEcoInterval(func, ...args) {
 | 
			
		||||
    return setInterval((...args) => {
 | 
			
		||||
        !document.hidden && func(...args)
 | 
			
		||||
    }, ...args)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								assets/public/vue.esm-browser.js
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								assets/public/vue.esm-browser.js
									
									
									
									
									
										Symbolic link
									
								
							@ -0,0 +1 @@
 | 
			
		||||
../node_modules/vue/dist/vue.esm-browser.js
 | 
			
		||||
							
								
								
									
										1
									
								
								assets/public/vue.esm-browser.prod.js
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								assets/public/vue.esm-browser.prod.js
									
									
									
									
									
										Symbolic link
									
								
							@ -0,0 +1 @@
 | 
			
		||||
../node_modules/vue/dist/vue.esm-browser.prod.js
 | 
			
		||||
		Reference in New Issue
	
	Block a user