playlist editor draft

This commit is contained in:
bkfox 2022-12-10 03:27:27 +01:00
parent 80cd5baa18
commit cfc0e45439
35 changed files with 13605 additions and 56 deletions

View File

@ -1,5 +1,3 @@
from copy import copy
from django.contrib import admin
from django.forms import ModelForm
from django.utils.translation import gettext as _
@ -60,11 +58,21 @@ class EpisodeAdminForm(ModelForm):
class EpisodeAdmin(SortableAdminBase, PageAdmin):
form = EpisodeAdminForm
list_display = PageAdmin.list_display
list_filter = tuple(f for f in PageAdmin.list_filter if f != 'pub_date') + \
('diffusion__start', 'pub_date')
list_filter = tuple(f for f in PageAdmin.list_filter
if f != 'pub_date') + ('diffusion__start', 'pub_date')
search_fields = PageAdmin.search_fields + ('parent__title',)
# readonly_fields = ('parent',)
inlines = [TrackInline, SoundInline, DiffusionInline]
def add_view(self, request, object_id, form_url='', context=None):
context = context or {}
context['init_app'] = True
context['init_el'] = '#inline-tracks'
return super().change_view(request, object_id, form_url, context)
def change_view(self, request, object_id, form_url='', context=None):
context = context or {}
context['init_app'] = True
context['init_el'] = '#inline-tracks'
return super().change_view(request, object_id, form_url, context)

View File

@ -13,10 +13,11 @@ class TrackInline(SortableInlineAdminMixin, admin.TabularInline):
template = 'admin/aircox/playlist_inline.html'
model = Track
extra = 0
fields = ('position', 'artist', 'title', 'info', 'tags')
fields = ('position', 'artist', 'title', 'tags', 'album', 'year', 'info')
list_display = ['artist', 'album', 'title', 'tags', 'related']
list_filter = ['artist', 'album', 'title', 'tags']
list_display = ['artist', 'title', 'tags', 'related']
list_filter = ['artist', 'title', 'tags']
class SoundTrackInline(TrackInline):
fields = TrackInline.fields + ('timestamp',)
@ -24,14 +25,15 @@ class SoundTrackInline(TrackInline):
class SoundInline(admin.TabularInline):
model = Sound
fields = ['type', 'name', 'audio', 'duration', 'is_good_quality', 'is_public',
'is_downloadable']
fields = ['type', 'name', 'audio', 'duration', 'is_good_quality',
'is_public', 'is_downloadable']
readonly_fields = ['type', 'audio', 'duration', 'is_good_quality']
extra = 0
max_num = 0
def audio(self, obj):
return mark_safe('<audio src="{}" controls></audio>'.format(obj.file.url))
return mark_safe('<audio src="{}" controls></audio>'
.format(obj.file.url))
audio.short_description = _('Audio')
def get_queryset(self, request):
@ -63,7 +65,8 @@ class SoundAdmin(SortableAdminBase, admin.ModelAdmin):
related.short_description = _('Program / Episode')
def audio(self, obj):
return mark_safe('<audio src="{}" controls></audio>'.format(obj.file.url)) \
return mark_safe('<audio src="{}" controls></audio>'
.format(obj.file.url)) \
if obj.type != Sound.TYPE_REMOVED else ''
audio.short_description = _('Audio')
@ -73,13 +76,15 @@ class TrackAdmin(admin.ModelAdmin):
def tag_list(self, obj):
return u", ".join(o.name for o in obj.tags.all())
list_display = ['pk', 'artist', 'title', 'tag_list', 'episode', 'sound', 'ts']
list_display = ['pk', 'artist', 'title', 'tag_list', 'episode',
'sound', 'ts']
list_editable = ['artist', 'title']
list_filter = ['artist', 'title', 'tags']
search_fields = ['artist', 'title']
fieldsets = [
(_('Playlist'), {'fields': ['episode', 'sound', 'position', 'timestamp']}),
(_('Playlist'), {'fields': ['episode', 'sound', 'position',
'timestamp']}),
(_('Info'), {'fields': ['artist', 'title', 'info', 'tags']}),
]
@ -92,8 +97,6 @@ class TrackAdmin(admin.ModelAdmin):
h = math.floor(ts / 3600)
m = math.floor((ts - h) / 60)
s = ts-h*3600-m*60
return '{:0>2}:{:0>2}:{:0>2}'.format(h,m,s)
return '{:0>2}:{:0>2}:{:0>2}'.format(h, m, s)
ts.short_description = _('timestamp')

View File

@ -219,7 +219,6 @@ class MonitorHandler(PatternMatchingEventHandler):
"""
self.subdir = subdir
self.pool = pool
if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR:
self.sound_kwargs = {'type': Sound.TYPE_ARCHIVE}
else:

View File

@ -246,7 +246,10 @@ class Track(models.Model):
)
title = models.CharField(_('title'), max_length=128)
artist = models.CharField(_('artist'), max_length=128)
tags = TaggableManager(verbose_name=_('tags'), blank=True,)
album = models.CharField(_('album'), max_length=128, null=True, blank=True)
tags = TaggableManager(verbose_name=_('tags'), blank=True)
year = models.IntegerField(_('year'), blank=True, null=True)
# FIXME: remove?
info = models.CharField(
_('information'),
max_length=128,

View File

@ -1,9 +1,12 @@
from rest_framework import serializers
from taggit.serializers import TagListSerializerField, TaggitSerializer
from .models import Diffusion, Log, Sound
from .models import Diffusion, Log, Sound, Track
__all__ = ['LogInfo', 'LogInfoSerializer']
__all__ = ['LogInfo', 'LogInfoSerializer', 'SoundSerializer',
'PodcastSerializer',
'AdminTrackSerializer']
class LogInfo:
@ -55,12 +58,13 @@ class LogInfoSerializer(serializers.Serializer):
class SoundSerializer(serializers.ModelSerializer):
file = serializers.FileField(use_url=False)
class Meta:
model = Sound
fields = ['pk', 'name', 'program', 'episode', 'type', 'file',
'duration', 'mtime', 'is_good_quality', 'is_public', 'url']
class PodcastSerializer(serializers.ModelSerializer):
# serializers.HyperlinkedIdentityField(view_name='sound', format='html')
@ -69,3 +73,11 @@ class PodcastSerializer(serializers.ModelSerializer):
fields = ['pk', 'name', 'program', 'episode', 'type',
'duration', 'mtime', 'url', 'is_downloadable']
class AdminTrackSerializer(TaggitSerializer, serializers.ModelSerializer):
tags = TagListSerializerField()
class Meta:
model = Track
fields = ('pk', 'artist', 'title', 'album', 'year', 'position',
'info', 'tags', 'episode', 'sound')

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Vue App</title>
<script defer src="js/chunk-vendors.js"></script><script defer src="js/chunk-common.js"></script><script defer src="js/admin.js"></script><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/chunk-common.css" rel="stylesheet"><link href="css/admin.css" rel="stylesheet"></head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Vue App</title>
<script defer src="js/chunk-vendors.js"></script><script defer src="js/chunk-common.js"></script><script defer src="js/core.js"></script><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/chunk-common.css" rel="stylesheet"></head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -1 +1,24 @@
.admin .navbar .navbar-brand{padding-right:1em}.admin .navbar .navbar-brand img{margin:0 .4em;margin-top:.3em;max-height:3em}.admin .breadcrumbs{margin-bottom:1em}.admin .results>#result_list{width:100%;margin:1em 0}.admin ul.menu-list li{list-style-type:none}.admin .submit-row a.deletelink{height:35px}
/*!*************************************************************************************************************************************************************************************************************************************!*\
!*** css ./node_modules/css-loader/dist/cjs.js??clonedRuleSet-24.use[1]!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-24.use[2]!./node_modules/sass-loader/dist/cjs.js??clonedRuleSet-24.use[3]!./src/assets/admin.scss ***!
\*************************************************************************************************************************************************************************************************************************************/
.admin .navbar .navbar-brand {
padding-right: 1em;
}
.admin .navbar .navbar-brand img {
margin: 0em 0.4em;
margin-top: 0.3em;
max-height: 3em;
}
.admin .breadcrumbs {
margin-bottom: 1em;
}
.admin .results > #result_list {
width: 100%;
margin: 1em 0em;
}
.admin ul.menu-list li {
list-style-type: none;
}
.admin .submit-row a.deletelink {
height: 35px;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,225 @@
(function(){"use strict";var n={5159:function(n,t,e){e(9651),e(8880);var o=e(9643),r=e(1784);const i={...o.Z,components:{...o.Z.components,...r.S}};window.App=i},1784:function(n,t,e){e.d(t,{S:function(){return v}});var o=e(4156),r=e(1847),i=e(6294),u=e(5189),c=e(2530),f=e(6306),a=e(7079),s=e(7467),l=e(8833),p=e(5127);t["Z"]={AAutocomplete:o.Z,AEpisode:r.Z,AList:i.Z,APage:u.Z,APlayer:c.C,APlaylist:f.Z,AProgress:a.Z,ASoundItem:s.Z};const v={AStatistics:l.Z,AStreamer:p.Z}}},t={};function e(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return n[o](i,i.exports,e),i.exports}e.m=n,function(){var n=[];e.O=function(t,o,r,i){if(!o){var u=1/0;for(s=0;s<n.length;s++){o=n[s][0],r=n[s][1],i=n[s][2];for(var c=!0,f=0;f<o.length;f++)(!1&i||u>=i)&&Object.keys(e.O).every((function(n){return e.O[n](o[f])}))?o.splice(f--,1):(c=!1,i<u&&(u=i));if(c){n.splice(s--,1);var a=r();void 0!==a&&(t=a)}}return t}i=i||0;for(var s=n.length;s>0&&n[s-1][2]>i;s--)n[s]=n[s-1];n[s]=[o,r,i]}}(),function(){e.d=function(n,t){for(var o in t)e.o(t,o)&&!e.o(n,o)&&Object.defineProperty(n,o,{enumerable:!0,get:t[o]})}}(),function(){e.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"===typeof window)return window}}()}(),function(){e.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)}}(),function(){e.r=function(n){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})}}(),function(){var n={328:0};e.O.j=function(t){return 0===n[t]};var t=function(t,o){var r,i,u=o[0],c=o[1],f=o[2],a=0;if(u.some((function(t){return 0!==n[t]}))){for(r in c)e.o(c,r)&&(e.m[r]=c[r]);if(f)var s=f(e)}for(t&&t(o);a<u.length;a++)i=u[a],e.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return e.O(s)},o=self["webpackChunkaircox_assets"]=self["webpackChunkaircox_assets"]||[];o.forEach(t.bind(null,0)),o.push=t.bind(null,o.push.bind(o))}();var o=e.O(void 0,[998,64],(function(){return e(5159)}));o=e.O(o)})();
//# sourceMappingURL=admin.js.map
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (function() { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./src/admin.js":
/*!**********************!*\
!*** ./src/admin.js ***!
\**********************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _assets_styles_scss__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./assets/styles.scss */ \"./src/assets/styles.scss\");\n/* harmony import */ var _assets_admin_scss__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./assets/admin.scss */ \"./src/assets/admin.scss\");\n/* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./index.js */ \"./src/index.js\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n\n\n\n\n\nconst AdminApp = {\n ..._app__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n components: {\n ..._app__WEBPACK_IMPORTED_MODULE_3__[\"default\"].components,\n ..._components__WEBPACK_IMPORTED_MODULE_4__.admin\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (AdminApp);\nwindow.App = AdminApp;\n\n//# sourceURL=webpack://aircox-assets/./src/admin.js?");
/***/ }),
/***/ "./src/assets/admin.scss":
/*!*******************************!*\
!*** ./src/assets/admin.scss ***!
\*******************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extract-plugin\n\n\n//# sourceURL=webpack://aircox-assets/./src/assets/admin.scss?");
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ id: moduleId,
/******/ loaded: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/************************************************************************/
/******/ /* webpack/runtime/chunk loaded */
/******/ !function() {
/******/ var deferred = [];
/******/ __webpack_require__.O = function(result, chunkIds, fn, priority) {
/******/ if(chunkIds) {
/******/ priority = priority || 0;
/******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
/******/ deferred[i] = [chunkIds, fn, priority];
/******/ return;
/******/ }
/******/ var notFulfilled = Infinity;
/******/ for (var i = 0; i < deferred.length; i++) {
/******/ var chunkIds = deferred[i][0];
/******/ var fn = deferred[i][1];
/******/ var priority = deferred[i][2];
/******/ var fulfilled = true;
/******/ for (var j = 0; j < chunkIds.length; j++) {
/******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every(function(key) { return __webpack_require__.O[key](chunkIds[j]); })) {
/******/ chunkIds.splice(j--, 1);
/******/ } else {
/******/ fulfilled = false;
/******/ if(priority < notFulfilled) notFulfilled = priority;
/******/ }
/******/ }
/******/ if(fulfilled) {
/******/ deferred.splice(i--, 1)
/******/ var r = fn();
/******/ if (r !== undefined) result = r;
/******/ }
/******/ }
/******/ return result;
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/compat get default export */
/******/ !function() {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function() { return module['default']; } :
/******/ function() { return module; };
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ !function() {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = function(exports, definition) {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/global */
/******/ !function() {
/******/ __webpack_require__.g = (function() {
/******/ if (typeof globalThis === 'object') return globalThis;
/******/ try {
/******/ return this || new Function('return this')();
/******/ } catch (e) {
/******/ if (typeof window === 'object') return window;
/******/ }
/******/ })();
/******/ }();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ !function() {
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
/******/ }();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ !function() {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/node module decorator */
/******/ !function() {
/******/ __webpack_require__.nmd = function(module) {
/******/ module.paths = [];
/******/ if (!module.children) module.children = [];
/******/ return module;
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/jsonp chunk loading */
/******/ !function() {
/******/ // no baseURI
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "admin": 0
/******/ };
/******/
/******/ // no chunk on demand loading
/******/
/******/ // no prefetching
/******/
/******/ // no preloaded
/******/
/******/ // no HMR
/******/
/******/ // no HMR manifest
/******/
/******/ __webpack_require__.O.j = function(chunkId) { return installedChunks[chunkId] === 0; };
/******/
/******/ // install a JSONP callback for chunk loading
/******/ var webpackJsonpCallback = function(parentChunkLoadingFunction, data) {
/******/ var chunkIds = data[0];
/******/ var moreModules = data[1];
/******/ var runtime = data[2];
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0;
/******/ if(chunkIds.some(function(id) { return installedChunks[id] !== 0; })) {
/******/ for(moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(runtime) var result = runtime(__webpack_require__);
/******/ }
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ installedChunks[chunkId][0]();
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ return __webpack_require__.O(result);
/******/ }
/******/
/******/ var chunkLoadingGlobal = self["webpackChunkaircox_assets"] = self["webpackChunkaircox_assets"] || [];
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/ }();
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module depends on other loaded chunks and execution need to be delayed
/******/ var __webpack_exports__ = __webpack_require__.O(undefined, ["chunk-vendors","chunk-common"], function() { return __webpack_require__("./src/admin.js"); })
/******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
/******/
/******/ })()
;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,215 @@
(function(){"use strict";var n={1784:function(n,t,e){var r=e(4156),o=e(1847),i=e(6294),u=e(5189),f=e(2530),c=e(6306),a=e(7079),s=e(7467),l=e(8833),p=e(5127);t["Z"]={AAutocomplete:r.Z,AEpisode:o.Z,AList:i.Z,APage:u.Z,APlayer:f.C,APlaylist:c.Z,AProgress:a.Z,ASoundItem:s.Z};l.Z,p.Z},5288:function(n,t,e){e(8880);var r=e(9643);window.App=r.Z}},t={};function e(r){var o=t[r];if(void 0!==o)return o.exports;var i=t[r]={exports:{}};return n[r](i,i.exports,e),i.exports}e.m=n,function(){var n=[];e.O=function(t,r,o,i){if(!r){var u=1/0;for(s=0;s<n.length;s++){r=n[s][0],o=n[s][1],i=n[s][2];for(var f=!0,c=0;c<r.length;c++)(!1&i||u>=i)&&Object.keys(e.O).every((function(n){return e.O[n](r[c])}))?r.splice(c--,1):(f=!1,i<u&&(u=i));if(f){n.splice(s--,1);var a=o();void 0!==a&&(t=a)}}return t}i=i||0;for(var s=n.length;s>0&&n[s-1][2]>i;s--)n[s]=n[s-1];n[s]=[r,o,i]}}(),function(){e.d=function(n,t){for(var r in t)e.o(t,r)&&!e.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:t[r]})}}(),function(){e.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"===typeof window)return window}}()}(),function(){e.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)}}(),function(){e.r=function(n){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})}}(),function(){var n={321:0};e.O.j=function(t){return 0===n[t]};var t=function(t,r){var o,i,u=r[0],f=r[1],c=r[2],a=0;if(u.some((function(t){return 0!==n[t]}))){for(o in f)e.o(f,o)&&(e.m[o]=f[o]);if(c)var s=c(e)}for(t&&t(r);a<u.length;a++)i=u[a],e.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return e.O(s)},r=self["webpackChunkaircox_assets"]=self["webpackChunkaircox_assets"]||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))}();var r=e.O(void 0,[998,64],(function(){return e(5288)}));r=e.O(r)})();
//# sourceMappingURL=core.js.map
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (function() { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./src/core.js":
/*!*********************!*\
!*** ./src/core.js ***!
\*********************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./index.js */ \"./src/index.js\");\n/* harmony import */ var _app_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./app.js */ \"./src/app.js\");\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (_app_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"]);\nwindow.App = _app_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"];\n\n//# sourceURL=webpack://aircox-assets/./src/core.js?");
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ id: moduleId,
/******/ loaded: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/************************************************************************/
/******/ /* webpack/runtime/chunk loaded */
/******/ !function() {
/******/ var deferred = [];
/******/ __webpack_require__.O = function(result, chunkIds, fn, priority) {
/******/ if(chunkIds) {
/******/ priority = priority || 0;
/******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
/******/ deferred[i] = [chunkIds, fn, priority];
/******/ return;
/******/ }
/******/ var notFulfilled = Infinity;
/******/ for (var i = 0; i < deferred.length; i++) {
/******/ var chunkIds = deferred[i][0];
/******/ var fn = deferred[i][1];
/******/ var priority = deferred[i][2];
/******/ var fulfilled = true;
/******/ for (var j = 0; j < chunkIds.length; j++) {
/******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every(function(key) { return __webpack_require__.O[key](chunkIds[j]); })) {
/******/ chunkIds.splice(j--, 1);
/******/ } else {
/******/ fulfilled = false;
/******/ if(priority < notFulfilled) notFulfilled = priority;
/******/ }
/******/ }
/******/ if(fulfilled) {
/******/ deferred.splice(i--, 1)
/******/ var r = fn();
/******/ if (r !== undefined) result = r;
/******/ }
/******/ }
/******/ return result;
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/compat get default export */
/******/ !function() {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function() { return module['default']; } :
/******/ function() { return module; };
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ !function() {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = function(exports, definition) {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/global */
/******/ !function() {
/******/ __webpack_require__.g = (function() {
/******/ if (typeof globalThis === 'object') return globalThis;
/******/ try {
/******/ return this || new Function('return this')();
/******/ } catch (e) {
/******/ if (typeof window === 'object') return window;
/******/ }
/******/ })();
/******/ }();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ !function() {
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
/******/ }();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ !function() {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/node module decorator */
/******/ !function() {
/******/ __webpack_require__.nmd = function(module) {
/******/ module.paths = [];
/******/ if (!module.children) module.children = [];
/******/ return module;
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/jsonp chunk loading */
/******/ !function() {
/******/ // no baseURI
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "core": 0
/******/ };
/******/
/******/ // no chunk on demand loading
/******/
/******/ // no prefetching
/******/
/******/ // no preloaded
/******/
/******/ // no HMR
/******/
/******/ // no HMR manifest
/******/
/******/ __webpack_require__.O.j = function(chunkId) { return installedChunks[chunkId] === 0; };
/******/
/******/ // install a JSONP callback for chunk loading
/******/ var webpackJsonpCallback = function(parentChunkLoadingFunction, data) {
/******/ var chunkIds = data[0];
/******/ var moreModules = data[1];
/******/ var runtime = data[2];
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0;
/******/ if(chunkIds.some(function(id) { return installedChunks[id] !== 0; })) {
/******/ for(moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(runtime) var result = runtime(__webpack_require__);
/******/ }
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ installedChunks[chunkId][0]();
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ return __webpack_require__.O(result);
/******/ }
/******/
/******/ var chunkLoadingGlobal = self["webpackChunkaircox_assets"] = self["webpackChunkaircox_assets"] || [];
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/ }();
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module depends on other loaded chunks and execution need to be delayed
/******/ var __webpack_exports__ = __webpack_require__.O(undefined, ["chunk-vendors","chunk-common"], function() { return __webpack_require__("./src/core.js"); })
/******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
/******/
/******/ })()
;

View File

@ -1,7 +1,74 @@
{% comment %}Inline block to edit playlists{% endcomment %}
{% load static i18n %}
{% load aircox aircox_admin static i18n %}
{% with inline_admin_formset.formset.instance as playlist %}
{% include "adminsortable2/edit_inline/tabular-django-4.1.html" %}
{# include "adminsortable2/edit_inline/tabular-django-4.1.html" #}
{% with inline_admin_formset as admin_formset %}
{% with admin_formset.formset as formset %}
<div id="inline-tracks" class="box mb-5">
<h5 class="title is-4">{% trans "Playlist" %}</h5>
<script id="{{ formset.prefix }}-init-data">{
"items": [
{% for form in formset.forms %}
{{ form.initial|json }}
{% if not forloop.last %},{% endif %}
{% endfor %}
]
}
</script>
{{ admin_formset.non_form_errors }}
<a-playlist-editor>
<template v-slot:top="{items}">
<input type="hidden" name="{{ formset.prefix }}-TOTAL_FORMS"
:value="items.length || 0"/>
<input type="hidden" name="{{ formset.prefix }}-INITIAL_FORMS"
value="{{ formset.initial_form_count }}"/>
<input type="hidden" name="{{ formset.prefix }}-MIN_NUM_FORMS"
value="{{ formset.min_num }}"/>
<input type="hidden" name="{{ formset.prefix }}-MAX_NUM_FORMS"
value="{{ formset.max_num }}"/>
</template>
<template #rows-header-head>
<th style="max-width:2em" title="{% trans "Track Position" %}"
aria-description="{% trans "Track Position" %}">
<span class="icon">
<i class="fa fa-arrow-down-1-9"></i>
</span>
</th>
</template>
<template v-slot:row-head="{item,row}">
<td>
[[ row+1 ]]
<input type="hidden"
:name="'{{ formset.prefix }}-' + row + '-position'"
:value="row"/>
<input t-if="item.data.id" type="hidden"
:name="'{{ formset.prefix }}-' + row + '-id'"
:value="item.data.id"/>
{% for field in admin_formset.fields %}
{% if field.name != 'position' and field.widget.is_hidden %}
<input type="hidden"
:name="'{{ formset.prefix }}-' + row + '-{{ field.name }}'"
v-model="item.data[attr]"/>
{% endif %}
{% endfor %}
</td>
</template>
{% for field in admin_formset.fields %}
{% if not field.widget.is_hidden and not field.is_readonly %}
<template v-slot:row-{{ field.name }}="{item,col,row,value,attr,emit}">
<div class="control">
<input type="{{ widget.type }}" class="input half-field"
:name="'{{ formset.prefix }}-' + row + '-{{ field.name }}'"
v-model="item.data[attr]"
@change="emit('change', col)"/>
</div>
</template>
{% endif %}
{% endfor %}
</a-playlist-editor>
</div>
{% endwith %}
{% endwith %}

View File

@ -44,6 +44,9 @@
{% if not init_app %}
initBuilder: false,
{% endif %}
{% if init_el %}
el: "{{ init_el }}",
{% endif %}
})
{% endblock %}
})

View File

@ -1,9 +1,16 @@
from django import template
from django.contrib import admin
from ..serializers import AdminTrackSerializer
register = template.Library()
@register.simple_tag(name='get_admin_tools')
def do_get_admin_tools():
return admin.site.get_tools()
@register.filter(name='serialize_track')
def do_serialize_track(instance):
ser = AdminTrackSerializer(instance=instance)
return ser.data

View File

@ -24,8 +24,8 @@ from django.utils import timezone as tz
from aircox.models import Station, Episode, Diffusion, Track, Sound, Log
from aircox.utils import date_range
from aircox_streamer.controllers import Streamer
from aircox_streamer.controllers import Streamer
# force using UTC
tz.activate(pytz.UTC)

View File

@ -11,6 +11,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^6.0.0",
"core-js": "^3.8.3",
"lodash": "^4.17.21",
"vue": "^3.2.13"
},
"devDependencies": {

View File

@ -11,6 +11,7 @@ $menu-item-active-background-color: #d2d2d2;
//-- helpers/modifiers
.is-fullwidth { width: 100%; }
.is-fullheight { height: 100%; }
.is-fixed-bottom {
position: fixed;
bottom: 0;
@ -40,6 +41,19 @@ $menu-item-active-background-color: #d2d2d2;
.overflow-hidden.is-fullwidth { max-width: 100%; }
*[draggable="true"] {
cursor: move;
}
//-- forms
input.half-field:not(:active):not(:hover) {
border: none;
background-color: rgba(0,0,0,0);
cursor: pointer;
}
//-- animations
@keyframes blink {
from { opacity: 1; }
to { opacity: 0.4; }

View File

@ -1,19 +1,22 @@
<template>
<div>
<!-- FIXME: header and footer should be inside list tags -->
<slot name="header"></slot>
<ul :class="listClass">
<component :is="listTag" :class="listClass">
<template v-for="(item,index) in items" :key="index">
<li :class="itemClass" @click="select(index)">
<component :is="itemTag" :class="itemClass" @click="select(index)"
:draggable="orderable"
@dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop">
<slot name="item" :selected="index == selectedIndex" :set="set" :index="index" :item="item"></slot>
</li>
</component>
</template>
</ul>
</component>
<slot name="footer"></slot>
</div>
</template>
<script>
export default {
emits: ['select', 'unselect'],
emits: ['select', 'unselect', 'move'],
data() {
return {
selectedIndex: this.defaultIndex,
@ -25,6 +28,9 @@ export default {
itemClass: String,
defaultIndex: { type: Number, default: -1},
set: Object,
orderable: { type: Boolean, default: false },
itemTag: { default: 'li' },
listTag: { default: 'ul' },
},
computed: {
@ -61,6 +67,34 @@ export default {
this.$emit('unselect', { item: this.selected, index: this.selectedIndex});
this.selectedIndex = -1;
},
onDragStart(ev) {
const dataset = ev.target.dataset;
const data = `cell:${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('cell:'))
return
ev.preventDefault()
const from = Number(data.slice(5))
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>

View File

@ -0,0 +1,216 @@
<template>
<div class="playlist-editor">
<slot name="top" :set="set" :columns="columns" :items="items"/>
<div class="tabs">
<ul>
<li :class="{'is-active': mode == Modes.Text}"
@click="mode = Modes.Text">
<a>
<span class="icon is-small">
<i class="fa fa-pencil"></i>
</span>
Texte
</a>
</li>
<li :class="{'is-active': mode == Modes.List}"
@click="mode = Modes.List">
<a>
<span class="icon is-small">
<i class="fa fa-list"></i>
</span>
Liste
</a>
</li>
</ul>
</div>
<section class="page" v-show="mode == Modes.Text">
<textarea ref="textarea" class="is-fullwidth" style="height: 10em;"
@change="updateList"
/>
<div class="columns mt-2">
<div class="column field is-vcentered">
<label class="label is-inline mr-2"
style="vertical-align: middle">
Ordre</label>
<table class="table is-bordered is-inline-block"
style="vertical-align: middle">
<tr>
<a-row :columns="columns" :item="FormatLabels"
@move="formatMove" :orderable="true">
</a-row>
</tr>
</table>
</div>
<div class="column field is-vcentered">
<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" value="--" class="input is-inline"
@change="updateList()"/>
</div>
</div>
<div class="column"/>
</div>
</section>
<section class="page" v-show="mode == Modes.List">
<a-rows :set="set" :columns="columns" :labels="FormatLabels"
:allow-create="true"
:list-class="listClass" :item-class="itemClass"
:orderable="true" @move="listItemMove"
@cell="onCellEvent">
<template v-for="[name,slot] of rowsSlots" :key="slot"
v-slot:[slot]="data">
<slot :name="name" v-bind="data"/>
</template>
</a-rows>
</section>
<section class="page" v-show="mode == Modes.Settings">
</section>
<slot name="bottom" :set="set" :columns="columns" :items="items"/>
</div>
</template>
<script>
import {dropRightWhile} from 'lodash'
import {Set} from '../model'
import Track from '../track'
import ARow from './ARow.vue'
import ARows from './ARows.vue'
export const Modes = {
Text: 0, List: 1, Settings: 2,
}
const FormatLabels = {
artist: 'Artiste', album: 'Album', year: 'Année', tags: 'Tags',
title: 'Titre',
}
export default {
components: { ARow, ARows },
props: {
listClass: String,
itemClass: String,
},
data() {
return {
dataEl: String,
Modes: Modes,
FormatLabels: FormatLabels,
mode: Modes.Text,
set: new Set(Track),
columns: ['artist', 'title', 'tags', 'album', 'year'],
}
},
computed: {
items() {
return this.set.items
},
rowsSlots() {
return Object.keys(this.$slots)
.filter(x => x.startsWith('row-') || x.startsWith('rows-'))
.map(x => [x, x.startsWith('rows-') ? x.slice(5) : x])
},
},
methods: {
onCellEvent(event) {
switch(event.name) {
case 'change': this.updateInput();
break;
}
},
formatMove({from, to}) {
const value = this.columns[from]
this.columns.splice(from, 1)
this.columns.splice(to, 0, value)
this.updateList()
},
listItemMove({from, to, set}) {
set.move(from, to);
this.updateInput()
},
updateList() {
const items = this.toList(this.$refs.textarea.value,
this.$refs.sep.value)
this.set.reset(items)
},
updateInput() {
const input = this.toText(this.items, this.$refs.sep.value)
this.$refs.textarea.value = input
},
/**
* From input and separator, return list of items.
*/
toList(input, sep) {
var lines = input.split('\n')
var items = []
for(let line of lines) {
line = line.trim()
if(!line)
continue
var lineBits = line.split(sep)
var item = {}
for(var col in this.columns) {
if(col >= lineBits.length)
break
const attr = this.columns[col]
item[attr] = lineBits[col].trim()
}
item && items.push(item)
}
return items
},
/**
* From items and separator return a string
*/
toText(items, sep) {
var lines = []
sep = ` ${(sep || this.$refs.sep.value).trim()} `
for(let item of items) {
if(!item)
continue
var line = []
for(var col of this.columns)
line.push(item.data[col] || '')
line = dropRightWhile(line, x => !x)
lines.push(line.join(sep))
}
return lines.join('\n')
},
/**
* Load initial data
*/
loadData({items=[], errors, fieldErrors, ...data}) {
for(var item of items)
this.set.push(item)
},
},
mounted() {
if(this.dataEl) {
const el = document.getElementById(this.dataEl)
if(el) {
const data = JSON.parse(el.textContext)
loadData(data)
}
}
},
}
</script>

View File

@ -0,0 +1,78 @@
<template>
<tr>
<slot name="head" :item="item" :row="index"/>
<template v-for="(attr,col) in columns" :key="col">
<td :class="['cell', 'cell-' + attr]" :data-index="col"
:draggable="orderable"
@dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop">
<slot :name="attr" :item="item" :row="index" :col="col"
:data="itemData" :attr="attr" :emit="cellEmit"
:value="itemData && itemData[attr]">
{{ itemData && itemData[attr] }}
</slot>
</td>
</template>
<slot name="tail" :item="item" :row="index"/>
</tr>
</template>
<script>
import Model from '../model'
export default {
emit: ['move', 'cell'],
props: {
item: Object,
index: Number,
columns: Array,
orderable: {type: Boolean, default: false},
},
computed: {
itemData() {
return this.item instanceof Model ? this.item.data : this.item;
},
},
methods: {
/// Emit a 'cell' event.
/// Event data: `{index, name, data, item, attr}`
///
/// @param {Number} col: cell column's index
/// @param {String} name: cell's event name
/// @param {} data: cell's event data
cellEmit(name, col, data) {
this.$emit('cell', {
name, col, data,
item: this.item,
attr: this.columns[col],
})
},
onDragStart(ev) {
const dataset = ev.target.dataset;
const data = `cell:${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('cell:'))
return
ev.preventDefault()
this.$emit('move', {
from: Number(data.slice(5)),
to: Number(ev.target.dataset.index),
})
},
},
}
</script>

View File

@ -0,0 +1,91 @@
<template>
<table class="table is-stripped is-fullwidth">
<thead>
<tr>
<slot name="header-head"/>
<th v-for="col in columns" :key="col"
style="vertical-align: middle">{{ labels[col] }}</th>
<slot name="header-tail"/>
</tr>
</thead>
<tbody>
<slot name="head"/>
<template v-for="(item,index) in items" :key="index">
<a-row :item="item" :index="index" :columns="columns" :data-index="index"
:draggable="orderable"
@dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop"
@cell="onCellEvent(index, $event)">
<template v-for="[name,slot] of rowSlots" :key="slot" v-slot:[slot]="data">
<slot :name="name" v-bind="data"/>
</template>
</a-row>
</template>
<template v-if="allowCreate">
<a-row :item="extraItem" :index="items.length" :columns="columns"
@keypress.enter.stop.prevent="validateExtraCell">
<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 },
emit: ['cell'],
props: {
...AList.props,
columns: Array,
labels: Object,
allowCreate: Boolean,
},
data() {
return {
...super.data,
extraItem: new this.set.model(),
}
},
computed: {
rowSlots() {
return Object.keys(this.$slots).filter(x => x.startsWith('row-'))
.map(x => [x, x.slice(4)])
},
},
methods: {
validateExtraCell() {
if(!this.allowCreate)
return
this.set.push(this.extraItem)
this.extraItem = new this.set.model()
},
/// 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) {
this.$emit('cell', {
...event, row,
set: this.set
})
},
},
}
Component.props.itemTag.default = 'tr'
Component.props.listTag.default = 'tbody'
export default Component
</script>

View File

@ -4,6 +4,7 @@ import AList from './AList.vue'
import APage from './APage.vue'
import APlayer from './APlayer.vue'
import APlaylist from './APlaylist.vue'
import APlaylistEditor from './APlaylistEditor.vue'
import AProgress from './AProgress.vue'
import ASoundItem from './ASoundItem.vue'
import AStatistics from './AStatistics.vue'
@ -18,6 +19,6 @@ export default {
}
export const admin = {
AStatistics, AStreamer,
AStatistics, AStreamer, APlaylistEditor
}

View File

@ -32,7 +32,7 @@ window.aircox = {
* Initialize main application and player.
*/
init(props=null, {config=null, builder=null, initBuilder=true,
initPlayer=true, hotReload=false}={})
initPlayer=true, hotReload=false, el=null}={})
{
if(initPlayer) {
let playerBuilder = this.playerBuilder
@ -44,6 +44,9 @@ window.aircox = {
this.builder = builder
if(config || window.App)
builder.config = config || window.App
if(el)
builder.config.el = el
builder.title = document.title
builder.mount({props})

View File

@ -35,7 +35,7 @@ export default class Model {
* Instanciate model with provided data and options.
* By default `url` is taken from `data.url_`.
*/
constructor(data, {url=null, ...options}={}) {
constructor(data={}, {url=null, ...options}={}) {
this.url = url || data.url_;
this.options = options;
this.commit(data);
@ -133,6 +133,8 @@ export default class Model {
}
/**
* List of models
*/
@ -231,6 +233,25 @@ export class Set {
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 () {

7
assets/src/track.js Normal file
View File

@ -0,0 +1,7 @@
import Model from './model'
export default class Track extends Model {
static getId(data) { return data.pk }
}