playlist editor draft
This commit is contained in:
parent
80cd5baa18
commit
cfc0e45439
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
|
|
12
aircox/static/aircox/admin.html
Normal file
12
aircox/static/aircox/admin.html
Normal 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>
|
12
aircox/static/aircox/core.html
Normal file
12
aircox/static/aircox/core.html
Normal 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>
|
|
@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
@ -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__);
|
||||
/******/
|
||||
/******/ })()
|
||||
;
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -44,6 +44,9 @@
|
|||
{% if not init_app %}
|
||||
initBuilder: false,
|
||||
{% endif %}
|
||||
{% if init_el %}
|
||||
el: "{{ init_el }}",
|
||||
{% endif %}
|
||||
})
|
||||
{% endblock %}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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>
|
||||
|
|
216
assets/src/components/APlaylistEditor.vue
Normal file
216
assets/src/components/APlaylistEditor.vue
Normal 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>
|
78
assets/src/components/ARow.vue
Normal file
78
assets/src/components/ARow.vue
Normal 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>
|
91
assets/src/components/ARows.vue
Normal file
91
assets/src/components/ARows.vue
Normal 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>
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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})
|
||||
|
||||
|
|
|
@ -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
7
assets/src/track.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Model from './model'
|
||||
|
||||
export default class Track extends Model {
|
||||
static getId(data) { return data.pk }
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user