upgrade vue and assets

This commit is contained in:
bkfox
2022-03-18 02:53:54 +01:00
parent 5b788ca28f
commit adb10c3d95
76 changed files with 2453 additions and 11975 deletions

24
assets/README.md Normal file
View File

@ -0,0 +1,24 @@
# aircox-assets
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@ -1,29 +0,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;
}

View File

@ -1,9 +0,0 @@
import App from 'public/app';
import AStatistics from './statistics.vue';
export default {
...App,
components: {...App.components, AStatistics},
}

View File

@ -1,25 +0,0 @@
import '@fortawesome/fontawesome-free/css/all.min.css'
import '@fortawesome/fontawesome-free/css/fontawesome.min.css'
import AdminApp from './app';
import './admin.scss';
window.aircox_admin = {
/**
* Filter items in the parent navbar-dropdown for provided key event on text input
*/
filter_menu: function(event) {
var filter = new RegExp(event.target.value, 'gi');
var container = event.target.closest('.navbar-dropdown');
if(event.target.value)
for(var item of container.querySelectorAll('a.navbar-item'))
item.style.display = item.innerHTML.search(filter) == -1 ? 'none' : null;
else
for(var item of container.querySelectorAll('a.navbar-item'))
item.style.display = null;
},
}
window.AdminApp = AdminApp

5
assets/babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

19
assets/jsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

47
assets/package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "aircox-assets",
"version": "0.1.0",
"private": true,
"sideEffects": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.0.0",
"core-js": "^3.8.3",
"vue": "^3.2.13"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"sass-loader": "^12.6.0",
"bulma": "^0.9.3"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

View File

@ -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

BIN
assets/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1 @@
../node_modules/vue/dist/vue.esm-browser.js

View File

@ -0,0 +1 @@
../node_modules/vue/dist/vue.esm-browser.prod.js

15
assets/src/admin.js Normal file
View File

@ -0,0 +1,15 @@
import './assets/admin.scss'
import './index.js'
import App from './app';
import {admin as components} from './components'
const AdminApp = {
...App,
components: {...App.components, ...components},
}
export default AdminApp;
window.App = AdminApp

20
assets/src/app.js Normal file
View File

@ -0,0 +1,20 @@
import components from './components'
const App = {
el: '#app',
delimiters: ['[[', ']]'],
components: {...components},
computed: {
player() { return window.aircox.player; },
},
}
export const PlayerApp = {
el: '#player',
components: {...components},
}
export default App

View File

@ -15,11 +15,11 @@ export default class Builder {
/**
* Fetch app from remote and mount application.
*/
fetch(url, {el='app', ...options}={}) {
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')
let app = doc.querySelector(el)
content = app ? app.innerHTML : content
return this.mount({content, title: doc.title, reset:true, url })
})
@ -102,7 +102,7 @@ export default class Builder {
else
options = {...options, method: target.method, body: formData}
}
this.fetch(url, options).then(_ => this.historySave(url))
this.fetch(url, options).then(() => this.historySave(url))
event.preventDefault();
event.stopPropagation();
}

View File

@ -0,0 +1,31 @@
#app.admin {
.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;
}
}

View File

@ -2,9 +2,10 @@
@import "~bulma/sass/utilities/_all.sass";
@import "~bulma/sass/components/dropdown.sass";
@import './admin.scss';
$body-background-color: $light;
@import "~buefy/src/scss/components/_autocomplete.scss";
@import "~bulma";
//-- helpers/modifiers

View File

@ -1,9 +1,8 @@
<template>
<div class="control">
<datalist :id="listId">
<template v-for="item in items" :key="item.path">
<option :value="item[field]"></option>
</template>
<option v-for="item in items" :key="item.path"
:value="item[field]"></option>
</datalist>
<input type="text" :name="name" :placeholder="placeholder"
:list="listId" @keyup="onKeyUp"/>

View File

@ -5,12 +5,12 @@
</template>
<script>
import {Set} from './model';
import Sound from './sound';
import Page from './page';
import {Set} from '../model';
import Sound from '../sound';
import APage from './APage';
export default {
extends: Page,
extends: APage,
data() {
return {

View File

@ -2,7 +2,7 @@
<div>
<slot name="header"></slot>
<ul :class="listClass">
<template v-for="(item,index) in items">
<template v-for="(item,index) in items" :key="index">
<li :class="itemClass" @click="select(index)">
<slot name="item" :selected="index == selectedIndex" :set="set" :index="index" :item="item"></slot>
</li>
@ -13,6 +13,7 @@
</template>
<script>
export default {
emits: ['select', 'unselect'],
data() {
return {
selectedIndex: this.defaultIndex,
@ -42,17 +43,7 @@ export default {
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) {
remove(index, select=false) {
this.set.remove(index);
if(index < this.selectedIndex)
this.selectedIndex--;

View File

@ -1,7 +1,7 @@
<template>
<div class="player">
<div :class="['player-panels', panel ? 'is-open' : '']">
<Playlist ref="pin" class="player-panel menu" v-show="panel == 'pin'"
<APlaylist 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)"
@ -12,8 +12,8 @@
Pinned
</p>
</template>
</Playlist>
<Playlist ref="queue" class="player-panel menu" v-show="panel == 'queue'"
</APlaylist>
<APlaylist 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">
@ -23,7 +23,7 @@
Playlist
</p>
</template>
</Playlist>
</APlaylist>
</div>
<div class="player-bar media">
@ -39,9 +39,9 @@
</div>
<div class="media-content">
<slot name="content" :loaded='loaded' :live='live'></slot>
<Progress v-if="loaded && duration" :value="currentTime" :max="this.duration"
<AProgress v-if="loaded && duration" :value="currentTime" :max="this.duration"
:format="displayTime"
@select="audio.currentTime = $event"></Progress>
@select="audio.currentTime = $event"></AProgress>
</div>
<div class="media-right">
<button class="button has-text-weight-bold" v-if="loaded" @click="play()">
@ -69,12 +69,11 @@
</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';
import Live from '../live';
import Sound from '../sound';
import {Set} from '../model';
import APlaylist from './APlaylist';
import AProgress from './AProgress';
export const State = {
@ -84,15 +83,17 @@ export const State = {
}
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', e => {
audio.addEventListener('timeupdate', () => {
this.currentTime = this.audio.currentTime;
});
audio.addEventListener('durationchange', e => {
audio.addEventListener('durationchange', () => {
this.duration = Number.isFinite(this.audio.duration) ? this.audio.duration : null;
});
@ -273,13 +274,12 @@ export default {
if(event.type == 'ended' && (!this.playlist || this.playlist.selectNext() == -1))
this.play();
},
},
mounted() {
this.load();
},
components: { Playlist, Progress },
}
</script>

View File

@ -2,18 +2,19 @@
<div>
<slot name="header"></slot>
<ul :class="listClass">
<li v-for="(item,index) in items" :class="itemClass" @click="!hasAction('play') && select(index)">
<li v-for="(item,index) in items" :class="itemClass" @click="!hasAction('play') && select(index)"
:key="index">
<a :class="index == selectedIndex ? 'is-active' : ''">
<SoundItem
<ASoundItem
:data="item" :index="index" :player="player" :set="set"
@togglePlay="togglePlay(index)"
:actions="actions">
<template v-slot:actions="{loaded,set}">
<template v-slot:actions="{}">
<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>
</ASoundItem>
</a>
</li>
</ul>
@ -21,11 +22,13 @@
</div>
</template>
<script>
import List from './list';
import SoundItem from './soundItem';
import AList from './AList';
import ASoundItem from './ASoundItem';
export default {
extends: List,
extends: AList,
emits: [...AList.emits, 'remove'],
components: { ASoundItem },
props: {
actions: Array,
@ -53,6 +56,5 @@ export default {
this.select(index)
},
},
components: { List, SoundItem },
}
</script>

View File

@ -21,7 +21,7 @@
</slot>
</div>
<div class="media-right">
<button class="button" v-if="player.sets.pin != $parent.set" @click.stop="player.togglePin(item)">
<button class="button" v-if="player && 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>
@ -31,16 +31,16 @@
</div>
</template>
<script>
import Model from './model';
import Sound from './sound';
import Model from '../model';
import Sound from '../sound';
export default {
props: {
data: {type: Object, default: x => {}},
data: {type: Object, default: () => {}},
name: String,
player: Object,
page_url: String,
actions: {type:Array, default: x => []},
actions: {type:Array, default: () => []},
index: {type:Number, default: null},
},

View File

@ -5,8 +5,7 @@
</template>
<script>
const splitReg = new RegExp(`,\s*`, 'g');
const splitReg = new RegExp(',\\s*', 'g');
export default {
data() {
@ -20,7 +19,6 @@ export default {
const items = this.$el.querySelectorAll('input[name="data"]:checked')
const counts = {};
console.log(items)
for(var item of items)
if(item.value)
for(var tag of item.value.split(splitReg))
@ -28,12 +26,13 @@ export default {
this.counts = counts;
},
onclick(event) {
onclick() {
// TODO: row click => check checkbox
}
},
mounted() {
console.log(this.counts)
this.$refs.form.addEventListener('change', () => this.update())
this.update()
}

View 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>

View File

@ -0,0 +1,23 @@
import AAutocomplete from './AAutocomplete.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 AStatistics from './AStatistics.vue'
import AStreamer from './AStreamer.vue'
/**
* Core components
*/
export default {
AAutocomplete, AEpisode, AList, APage, APlayer, APlaylist,
AProgress, ASoundItem,
}
export const admin = {
AStatistics, AStreamer,
}

8
assets/src/core.js Normal file
View File

@ -0,0 +1,8 @@
import './index.js'
import App from './app.js'
export default App
window.App = App

View File

@ -13,7 +13,7 @@ import Builder from './appBuilder'
import Sound from './sound'
import {Set} from './model'
import './styles.scss'
import './assets/styles.scss'
window.aircox = {
@ -32,14 +32,17 @@ window.aircox = {
/**
* Initialize main application and player.
*/
init(props=null, {config=null, builder=null, initPlayer=true}={}) {
init(props=null, {config=null, builder=null, initPlayer=true, hotReload=false}={}) {
builder = builder || this.builder
this.builder = builder
if(config)
builder.config = config
if(config || window.App)
builder.config = config || window.App
builder.title = document.title
builder.mount({props})
if(hotReload)
builder.enableHotReload(hotReload)
if(initPlayer) {
let playerBuilder = this.playerBuilder
playerBuilder.mount()
@ -47,14 +50,3 @@ window.aircox = {
},
}
/*
window.addEventListener('load', e => {
const [app, player] = [aircox.builder, aircox.playerBuilder]
app.title = document.title
app.mount()
app.enableHotReload(window)
player.mount()
})
*/

View File

@ -1,4 +1,4 @@
import {setEcoTimeout} from 'public/utils';
import {setEcoTimeout} from './utils';
import Model from './model';
export default class Live {
@ -38,7 +38,7 @@ export default class Live {
refresh() {
const promise = this.fetch();
promise.then(data => {
promise.then(() => {
if(promise != this.promise)
return [];

View File

@ -1,15 +1,24 @@
function getCookie(name) {
/**
* Return cookie with provided key
*/
function getCookie(key) {
if(document.cookie && document.cookie !== '') {
const cookie = document.cookie.split(';')
.find(c => c.trim().startsWith(name + '='))
.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')
@ -18,9 +27,17 @@ export function getCsrf() {
// TODO: prevent duplicate simple fetch
/**
* Provide interface used to fetch and manipulate objects.
*/
export default class Model {
constructor(data, {url=null}={}) {
/**
* 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);
}
@ -45,8 +62,13 @@ export default class Model {
}
}
static fromList(items, args=null) {
return items ? items.map(d => new this(d, args)) : []
/**
* 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=null) {
return items ? items.map(d => new this(d, options)) : []
}
/**

View File

@ -1,4 +1,4 @@
import Model, {Set} from './model';
import Model from './model';
export default class Sound extends Model {

View File

@ -1,5 +1,5 @@
import Model from 'public/model';
import {setEcoInterval} from 'public/utils';
import Model from './model';
import {setEcoInterval} from './utils';
export class Streamer extends Model {
@ -18,6 +18,8 @@ export class Streamer extends Model {
}
}
export default Streamer;
export class Request extends Model {
static getId(data) { return data.rid; }
}

View File

@ -1,7 +1,7 @@
import AdminApp from 'admin/app';
import Model, {Set} from 'public/model';
import Sound from 'public/sound';
import {setEcoInterval} from 'public/utils';
import AdminApp from '../admin';
import Model from '../model';
import Sound from '../sound';
import {setEcoInterval} from '../utils';
import {Streamer, Queue} from './controllers';

View File

@ -1,16 +1,19 @@
import AdminApp from 'admin/app';
import Model, {Set} from 'public/model';
import Sound from 'public/sound';
import {setEcoInterval} from 'public/utils';
<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, Queue} from './controllers';
import {Streamer} from './controllers';
export const StreamerApp = {
...AdminApp,
export default {
props: {
...(AdminApp.props || {}),
apiUrl: String,
},
@ -27,8 +30,6 @@ export const StreamerApp = {
},
computed: {
...(AdminApp.computed || {}),
sources() {
var sources = this.streamer ? this.streamer.sources : [];
return sources.filter(s => s.data)
@ -36,8 +37,6 @@ export const StreamerApp = {
},
methods: {
...(AdminApp.methods || {}),
fetchStreamers() {
Streamer.fetch(this.apiUrl, {many:true}).then(streamers => {
this.streamers = streamers
@ -56,6 +55,4 @@ export const StreamerApp = {
clearInterval(this.fetchInterval)
}
}
window.StreamerApp = StreamerApp
</script>

View File

@ -1,12 +1,15 @@
/**
* 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)

22
assets/vue.config.js Normal file
View File

@ -0,0 +1,22 @@
const path = require('path');
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
outputDir: path.resolve('../aircox/static/aircox'),
publicPath: './',
runtimeCompiler: true,
filenameHashing: false,
css: {
extract: true,
loaderOptions: {
sass: { sourceMap: true },
}
},
pages: {
core: { entry: 'src/core.js', },
admin: { entry: 'src/admin.js' },
}
})