work on playlist & tracklist

This commit is contained in:
bkfox 2024-03-23 17:22:52 +01:00
parent 21f856e731
commit f41cc3ce0c
21 changed files with 451 additions and 142 deletions

View File

@ -1,12 +1,11 @@
from django import forms from django import forms
from django.forms.models import modelformset_factory
from filer.models.filemodels import File
from aircox import models from aircox import models
from aircox.controllers.sound_file import SoundFile
__all__ = ("CommentForm", "PageForm", "ProgramForm", "EpisodeForm") __all__ = ("CommentForm", "PageForm", "ProgramForm", "EpisodeForm", "SoundForm", "TrackFormSet")
class CommentForm(forms.ModelForm): class CommentForm(forms.ModelForm):
@ -40,18 +39,52 @@ class ProgramForm(PageForm):
class EpisodeForm(PageForm): class EpisodeForm(PageForm):
new_podcast = forms.FileField(required=False)
class Meta: class Meta:
model = models.Episode model = models.Episode
fields = PageForm.Meta.fields fields = PageForm.Meta.fields
def save(self, commit=True):
file_obj = self.cleaned_data["new_podcast"] # def save(self, commit=True):
if file_obj: # file_obj = self.cleaned_data["new_podcast"]
obj, _ = File.objects.get_or_create(original_filename=file_obj.name, file=file_obj) # if file_obj:
sound_file = SoundFile(obj.path) # obj, _ = File.objects.get_or_create(original_filename=file_obj.name, file=file_obj)
sound_file.sync( # sound_file = SoundFile(obj.path)
program=self.instance.program, episode=self.instance, type=0, is_public=True, is_downloadable=True # sound_file.sync(
) # program=self.instance.program, episode=self.instance, type=0, is_public=True, is_downloadable=True
super().save(commit=commit) # )
# super().save(commit=commit)
class SoundForm(forms.ModelForm):
"""SoundForm used in EpisodeUpdateView."""
class Meta:
model = models.Sound
fields = ["name", "program", "episode", "type", "position", "duration", "is_public", "is_downloadable"]
TrackFormSet = modelformset_factory(
models.Track,
fields=[
"position",
"artist",
"title",
"tags",
"album",
],
extra=0,
)
"""Track formset used in EpisodeUpdateView."""
SoundFormSet = modelformset_factory(
models.Sound,
fields=[
"position",
"name",
"type",
"is_public",
"is_downloadable",
],
extra=0,
)
"""Sound formset used in EpisodeUpdateView."""

View File

@ -148,12 +148,12 @@ class Sound(models.Model):
) )
is_public = models.BooleanField( is_public = models.BooleanField(
_("public"), _("public"),
help_text=_("whether it is publicly available as podcast"), help_text=_("sound is available as podcast"),
default=False, default=False,
) )
is_downloadable = models.BooleanField( is_downloadable = models.BooleanField(
_("downloadable"), _("downloadable"),
help_text=_("whether it can be publicly downloaded by visitors (sound must be " "public)"), help_text=_("sound can be downloaded by visitors (sound must be public)"),
default=False, default=False,
) )

View File

@ -16,7 +16,7 @@
\**********************/ \**********************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _styles_admin_scss__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./styles/admin.scss */ \"./src/styles/admin.scss\");\n/* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./index.js */ \"./src/index.js\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n/* harmony import */ var _track__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./track */ \"./src/track.js\");\n\n\n\n\n\nconst AdminApp = {\n ..._app__WEBPACK_IMPORTED_MODULE_2__[\"default\"],\n components: {\n ..._app__WEBPACK_IMPORTED_MODULE_2__[\"default\"].components,\n ..._components__WEBPACK_IMPORTED_MODULE_3__.admin\n },\n data() {\n return {\n ...super.data,\n Track: _track__WEBPACK_IMPORTED_MODULE_4__[\"default\"]\n };\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (AdminApp);\nwindow.App = AdminApp;\n\n//# sourceURL=webpack://aircox-assets/./src/admin.js?"); eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _styles_admin_scss__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./styles/admin.scss */ \"./src/styles/admin.scss\");\n/* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./index.js */ \"./src/index.js\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n\n\n\n\nconst AdminApp = {\n ..._app__WEBPACK_IMPORTED_MODULE_2__[\"default\"],\n components: {\n ..._app__WEBPACK_IMPORTED_MODULE_2__[\"default\"].components,\n ..._components__WEBPACK_IMPORTED_MODULE_3__.admin\n },\n data() {\n return {\n ...super.data\n };\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (AdminApp);\nwindow.App = AdminApp;\n\n//# sourceURL=webpack://aircox-assets/./src/admin.js?");
/***/ }) /***/ })

File diff suppressed because one or more lines are too long

View File

@ -2,8 +2,29 @@
{% load static i18n humanize honeypot aircox %} {% load static i18n humanize honeypot aircox %}
{% block page_form %} {% block page_form %}
<a-modal ref="sound-edit-modal" title="{% translate "Edit sound" %}"> <a-modal ref="sound-edit-modal" v-if="item" title="{% translate "Edit sound" %}">
<template #default> <template #default="{item}">
{% for field in sound_form %}
{% if field.name in "episode,program" %}
<input type="hidden" name="{{ field.name }}" value="{{ field.value }}" />
{% else %}
<div class="field">
{% if field|is_checkbox %}
<label class="label">
<input type="text" name="{{ field.name }}" :value="item.{{ field.name }}">
{{ field.label }}
</label>
{% else %}
<label class="label">{{ field.label }}</label>
<div class="control">
<input type="text" name="{{ field.name }}" :value="item.{{ field.name }}">
</div>
{% endif %}
<p class="help">{{ field.help_text }}</p>
</div>
{% endif %}
{% endfor %}
</template> </template>
</a-modal> </a-modal>
@ -11,37 +32,11 @@
<template v-slot="{podcasts,page}"> <template v-slot="{podcasts,page}">
{{ block.super }} {{ block.super }}
<hr/> <hr/>
{% include "./widgets/tracklist_editor.html" with formset=playlist_formset %} {% include "./widgets/tracklist_editor.html" with formset=tracklist_formset %}
<hr/> <hr/>
<section class="container"> <section class="container">
<h3 class="title">{% translate "Sound files" %}</h3> <h3 class="title">{% translate "Sound files" %}</h3>
<a-playlist v-if="page" :set="podcasts" {% include "./widgets/playlist_editor.html" with formset=playlist_formset %}
name="{{ page.title }}"
list-class="menu-list" item-class="menu-item"
:player="player" :actions="['play']"
@select="player.playItems('queue', $event.item)">
<template #after-title="{item}">
<span class="flex-grow-1">
[[ item.data.type_display ]]
<span v-if="item.data.is_public">
/
{% translate "public" %}
</span>
<span v-if="item.data.is_downloadable">
/
{% translate "downloadable" %}
</span>
</span>
</template>
<template #actions="{item}">
<button type="button" class="button"
title="{% translate "Edit" %}">
<span class="icon">
<i class="fa fa-edit"></i>
</span>
</button>
</template>
</a-playlist>
</section> </section>
</template> </template>
</a-episode> </a-episode>

View File

@ -75,6 +75,7 @@
{{ field }} {{ field }}
{% endif %} {% endif %}
</div> </div>
<p class="help">{{ field.help_text }}</p>
</div> </div>
{% endif %} {% endif %}
{% if field.errors %} {% if field.errors %}

View File

@ -0,0 +1,21 @@
{% comment %}
Render a form field instance as field (to be used when no model instance is provided). Value is binded as vue, class to Bulma
Context:
- name: field name
- field: form field
- value: input ":value" attribute
{% endcomment %}
{% load aircox %}
{% if field|is_checkbox %}
<input type="checkbox" class="checkbox" name="{{ name }}" :checked="{{ value }}">
{% elif field|is_select %}
<select name="{{ name }}" class="select" :value="{{ value }}">
{% for value, label in field.widget.choices %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
{% else %}
<input type="text" class="input" name="{{ name }}" :value="{{ value }}">
{% endif %}

View File

@ -0,0 +1,74 @@
{% comment %}
Context:
- formset: formset
{% endcomment %}
{% load aircox aircox_admin static i18n %}
{% with formset.form.base_fields as fields %}
<div id="inline-sounds">
{{ formset.non_form_errors }}
<!-- formset.management_form -->
<a-playlist-editor
:labels="{% inline_labels %}"
:init-data="{% formset_inline_data formset=formset %}"
:default-columns="[{% for f in fields.keys %}{% if f != "position" %}'{{ f }}',{% endif %}{% endfor %}]"
settings-url="{% url "api:user-settings" %}"
data-prefix="{{ formset.prefix }}-">
<template #title>
<h3 class="title is-2">{% trans "Playlist" %}</h3>
</template>
<template #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 "Sound Position" %}"
aria-description="{% trans "Sound 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 || item.id"/>
{% for name, field in fields.items %}
{% if name != 'position' and field.widget.is_hidden %}
<input type="hidden"
:name="'{{ formset.prefix }}-' + row + '-{{ name }}'"
v-model="item.data[attr]"/>
{% endif %}
{% endfor %}
</td>
</template>
{% for name, field in fields.items %}
{% if not field.widget.is_hidden and not field.is_readonly %}
<template v-slot:row-{{ name }}="{item,cell,value,attr,emit}">
<div class="field">
<div class="control">
{% include "./form_field.html" with field=field name=name value="item.data."|add:name %}
</div>
<p v-for="error in item.error(attr)" class="help is-danger">
[[ error ]] !
</p>
</div>
</template>
{% endif %}
{% endfor %}
</a-playlist-editor>
</div>
{% endwith %}

View File

@ -5,15 +5,15 @@ Context:
{% endcomment %} {% endcomment %}
{% load aircox aircox_admin static i18n %} {% load aircox aircox_admin static i18n %}
{% with formset.form.fields as fields %} {% with formset.form.base_fields as fields %}
<div id="inline-tracks"> <div id="inline-tracks">
{{ formset.non_form_errors }} {{ formset.non_form_errors }}
<!-- formset.management_form --> <!-- formset.management_form -->
<a-tracklist-editor <a-tracklist-editor
:labels="{% track_inline_labels %}" :labels="{% inline_labels %}"
:init-data="{% track_inline_data formset=formset %}" :init-data="{% formset_inline_data formset=formset %}"
:default-columns="[{% for f in fields %}{% if f != "position" %}'{{ f }}',{% endif %}{% endfor %}]" :default-columns="[{% for f in fields.keys %}{% if f != "position" %}'{{ f }}',{% endif %}{% endfor %}]"
settings-url="{% url "api:user-settings" %}" settings-url="{% url "api:user-settings" %}"
data-prefix="{{ formset.prefix }}-"> data-prefix="{{ formset.prefix }}-">
<template #title> <template #title>
@ -47,25 +47,26 @@ Context:
:name="'{{ formset.prefix }}-' + row + '-id'" :name="'{{ formset.prefix }}-' + row + '-id'"
:value="item.data.id || item.id"/> :value="item.data.id || item.id"/>
{% for field in fields %} {% for name, field in fields.items %}
{% if field != 'position' and field.widget.is_hidden %} {% if name != 'position' and field.widget.is_hidden %}
<input type="hidden" <input type="hidden"
:name="'{{ formset.prefix }}-' + row + '-{{ field.name }}'" :name="'{{ formset.prefix }}-' + row + '-{{ name }}'"
v-model="item.data[attr]"/> v-model="item.data[attr]"/>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</td> </td>
</template> </template>
{% for field in fields %} {% for name, field in fields.items %}
{% if not field.widget.is_hidden and not field.is_readonly %} {% if not field.widget.is_hidden and not field.is_readonly %}
<template v-slot:row-{{ field }}="{item,cell,value,attr,emit}"> ---
<template v-slot:row-{{ name }}="{item,cell,value,attr,emit}">
<div class="field"> <div class="field">
<a-autocomplete <a-autocomplete
:input-class="['input', item.error(attr) ? 'is-danger' : 'half-field']" :input-class="['input', item.error(attr) ? 'is-danger' : 'half-field']"
url="{% url 'api:track-autocomplete' %}?{{ field }}=${query}&field={{ field }}" url="{% url 'api:track-autocomplete' %}?{{ name }}=${query}&field={{ name }}"
:name="'{{ formset.prefix }}-' + cell.row + '-{{ field }}'" :name="'{{ formset.prefix }}-' + cell.row + '-{{ name }}'"
v-model="item.data[attr]" v-model="item.data[attr]"
title="{{ field }}" title="{{ name }}"
@change="emit('change', col)"/> @change="emit('change', col)"/>
<p v-for="error in item.error(attr)" class="help is-danger"> <p v-for="error in item.error(attr)" class="help is-danger">
[[ error ]] ! [[ error ]] !

View File

@ -1,7 +1,7 @@
import json import json
import random import random
from django import template from django import template, forms
from django.contrib.admin.templatetags.admin_urls import admin_urlname from django.contrib.admin.templatetags.admin_urls import admin_urlname
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
@ -148,3 +148,15 @@ def do_edit_view(obj):
@register.filter(name="detail_view") @register.filter(name="detail_view")
def do_detail_view(obj): def do_detail_view(obj):
return "%s-detail" % obj.split("-")[0] return "%s-detail" % obj.split("-")[0]
@register.filter(name="is_checkbox")
def is_checkbox(field):
"""Return True if field is a checkbox."""
return isinstance(field.widget, forms.CheckboxInput)
@register.filter(name="is_select")
def is_select(field):
"""Return True if field is a select."""
return isinstance(field.widget, forms.Select)

View File

@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
from aircox.serializers.admin import UserSettingsSerializer from aircox.serializers.admin import UserSettingsSerializer
__all__ = ("register", "do_get_admin_tools", "do_track_inline_data") __all__ = ("register", "do_get_admin_tools", "do_formset_inline_data", "do_inline_labels")
register = template.Library() register = template.Library()
@ -17,14 +17,24 @@ def do_get_admin_tools():
return admin.site.get_tools() return admin.site.get_tools()
@register.simple_tag(name="track_inline_data", takes_context=True) @register.simple_tag(name="formset_inline_data", takes_context=True)
def do_track_inline_data(context, formset): def do_formset_inline_data(context, formset):
"""Return initial data for playlist editor as dict. Keys are: """Return initial data of formset as dict (used by TrackListEditor and
PlaylistEditor). Keys are:
- ``items``: list of items. Extra keys: - ``items``: list of items. Extra keys:
- ``__error__``: dict of form fields errors - ``__error__``: dict of form fields errors
- ``settings``: user's settings - ``settings``: user's settings
""" """
# --- get fields labels
model = formset.form.Meta.model
fields = {}
for field_name in formset.form.Meta.fields:
field = model._meta.get_field(field_name)
fields[field_name] = str(field.verbose_name).capitalize()
# --- get items
items = [] items = []
for form in formset.forms: for form in formset.forms:
item = {name: form[name].value() for name in form.fields.keys()} item = {name: form[name].value() for name in form.fields.keys()}
@ -36,7 +46,7 @@ def do_track_inline_data(context, formset):
item["tags"] = ", ".join(tag.name for tag in tags) item["tags"] = ", ".join(tag.name for tag in tags)
items.append(item) items.append(item)
data = {"items": items} data = {"items": items, "fields": fields}
user = context["request"].user user = context["request"].user
settings = getattr(user, "aircox_settings", None) settings = getattr(user, "aircox_settings", None)
data["settings"] = settings and UserSettingsSerializer(settings).data data["settings"] = settings and UserSettingsSerializer(settings).data
@ -44,22 +54,19 @@ def do_track_inline_data(context, formset):
return source return source
track_inline_labels_ = { inline_labels_ = {
"artist": _("Artist"), # list editor
"album": _("Album"), "add_item": _("Add an item"),
"title": _("Title"), "remove_item": _("Remove"),
"tags": _("Tags"),
"year": _("Year"),
"save_settings": _("Save Settings"), "save_settings": _("Save Settings"),
"discard_changes": _("Discard changes"), "discard_changes": _("Discard changes"),
# track list
"columns": _("Columns"), "columns": _("Columns"),
"add_track": _("Add a track"),
"remove_track": _("Remove"),
"timestamp": _("Timestamp"), "timestamp": _("Timestamp"),
} }
@register.simple_tag(name="track_inline_labels") @register.simple_tag(name="inline_labels")
def do_track_inline_labels(): def do_inline_labels():
"""Return labels for columns in playlist editor as dict.""" """Return labels for columns in playlist editor as dict."""
return json.dumps({k: str(v) for k, v in track_inline_labels_.items()}) return json.dumps({k: str(v) for k, v in inline_labels_.items()})

View File

@ -1,9 +1,8 @@
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.forms.models import modelformset_factory
from django.urls import reverse from django.urls import reverse
from aircox.forms import EpisodeForm
from aircox.models import Episode, Program, StaticPage, Track from aircox.models import Episode, Program, StaticPage, Track
from aircox import forms
from ..filters import EpisodeFilters from ..filters import EpisodeFilters
from .page import PageListView from .page import PageListView
from .program import ProgramPageDetailView, BaseProgramMixin from .program import ProgramPageDetailView, BaseProgramMixin
@ -49,18 +48,9 @@ class PodcastListView(EpisodeListView):
class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView): class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
model = Episode model = Episode
form_class = EpisodeForm form_class = forms.EpisodeForm
template_name = "aircox/episode_form.html" template_name = "aircox/episode_form.html"
playlist_fields = (
"position",
"artist",
"title",
"tags",
"album",
)
"""Playlist editor's ordered fields."""
def test_func(self): def test_func(self):
program = self.get_object().program program = self.get_object().program
return self.request.user.has_perm("aircox.%s" % program.change_permission_codename) return self.request.user.has_perm("aircox.%s" % program.change_permission_codename)
@ -68,16 +58,42 @@ class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
def get_success_url(self): def get_success_url(self):
return reverse("episode-detail", kwargs={"slug": self.get_object().slug}) return reverse("episode-detail", kwargs={"slug": self.get_object().slug})
def get_playlist_queryset(self, episode): def get_tracklist_queryset(self, episode):
return Track.objects.filter(episode=episode) return Track.objects.filter(episode=episode)
def get_tracklist_formset(self, episode, **kwargs):
kwargs.update(
{
"queryset": self.get_tracklist_queryset(episode),
"initial": {
"episode": episode.id,
},
}
)
return forms.TrackFormSet(**kwargs)
def get_playlist_queryset(self, episode):
return episode.sound_set.all()
def get_playlist_formset(self, episode, **kwargs): def get_playlist_formset(self, episode, **kwargs):
kwargs["queryset"] = self.get_playlist_queryset(episode) kwargs.update(
TrackFormSet = modelformset_factory(Track, fields=self.playlist_fields, extra=0) {
return TrackFormSet(**kwargs) "queryset": self.get_playlist_queryset(episode),
"initial": {
"program": episode.parent_id,
"episode": episode.id,
},
}
)
return forms.SoundFormSet(**kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs["playlist_formset"] = self.get_playlist_formset(self.object) kwargs.update(
{
"playlist_formset": self.get_playlist_formset(self.object),
"tracklist_formset": self.get_tracklist_formset(self.object),
}
)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):

View File

@ -3,7 +3,6 @@ import './index.js'
import App from './app'; import App from './app';
import {admin as components} from './components' import {admin as components} from './components'
import Track from './track'
const AdminApp = { const AdminApp = {
...App, ...App,
@ -12,7 +11,6 @@ const AdminApp = {
data() { data() {
return { return {
...super.data, ...super.data,
Track,
} }
} }
} }

View File

@ -6,12 +6,17 @@
<div class="modal-card-title"> <div class="modal-card-title">
<slot name="title">{{ title }}</slot> <slot name="title">{{ title }}</slot>
</div> </div>
<button type="button" class="delete square" aria-label="close" @click="close">
<span class="icon">
<i class="fa fa-close"></i>
</span>
</button>
</header> </header>
<section class="modal-card-body"> <section class="modal-card-body">
<slot name="default"></slot> <slot name="default" :item="item"></slot>
</section> </section>
<div class="modal-card-foot align-right"> <div class="modal-card-foot align-right">
<slot name="footer" :close="close"></slot> <slot name="footer" :item="item" :close="close"></slot>
</div> </div>
</div> </div>
</section> </section>
@ -24,13 +29,24 @@ export default {
data() { data() {
return { return {
///! If true, modal is open
active: false, active: false,
///! Item or data passed down to slots.
item: null,
} }
}, },
methods: { methods: {
open() { this.active = true; }, ///! Open modal dialog. Set provided `item` to dialog's one.
close() { this.active = false; }, open(item=null) {
this.active = true
this.item = item
},
///! Close modal and reset item to null.
close() {
this.active = false
this.item = null
},
} }
} }
</script> </script>

View File

@ -37,10 +37,6 @@
<span class="fas fa-pause" v-if="playing"></span> <span class="fas fa-pause" v-if="playing"></span>
<span class="fas fa-play" v-else></span> <span class="fas fa-play" v-else></span>
</button> </button>
<!--
<div class="media-cover" v-if="current && current.data.cover">
<img :src="current.data.cover" class="cover" />
</div> -->
<div :class="['a-player-bar-content', loaded && duration ? 'has-progress' : '']"> <div :class="['a-player-bar-content', loaded && duration ? 'has-progress' : '']">
<slot name="content" :loaded="loaded" :live="live" :current="current"></slot> <slot name="content" :loaded="loaded" :live="live" :current="current"></slot>
</div> </div>

View File

@ -0,0 +1,105 @@
<template>
<div class="a-playlist-editor">
<a-rows :set="set" :columns="columns"
:labels="initData.fields" :allow-create="true" :orderable="true"
@move="listItemMove">
<template v-for="[name,slot] of rowsSlots" :key="slot"
v-slot:[slot]="data">
<slot v-if="name != 'row-tail'" :name="name" v-bind="data"/>
</template>
</a-rows>
<div class="flex-row">
<div class="flex-grow-1 flex-row">
</div>
<div class="flex-grow-1 align-right">
<button type="button" class="button square is-warning p-2"
@click="loadData({items: this.initData.items},true)"
:title="labels.discard_changes"
:aria-label="labels.discard_changes"
>
<span class="icon"><i class="fa fa-rotate" /></span>
</button>
<button type="button" class="button square is-primary p-2"
@click="this.set.push(new this.set.model())"
:title="labels.add_sound"
:aria-label="labels.add_sound"
>
<span class="icon"><i class="fa fa-plus"/></span>
</button>
</div>
</div>
</div>
</template>
<script>
// import {dropRightWhile, cloneDeep, isEqual} from 'lodash'
import {cloneDeep} from 'lodash'
import Model, {Set} from '../model'
// import AActionButton from './AActionButton'
import ARows from './ARows'
// import AModal from "./AModal"
export default {
components: {ARows},
props: {
initData: Object,
dataPrefix: String,
labels: Object,
settingsUrl: String,
columns: {
type: Array,
default: () => ['name', "type", 'is_public', 'is_downloadable']
},
},
data() {
return {
set: new Set(Model),
}
},
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: {
listItemMove({from, to, set}) {
set.move(from, to);
},
/**
* Load initial data
*/
loadData({items=[] /*, settings=null*/}, reset=false) {
if(reset) {
this.set.items = []
}
for(var index in items)
this.set.push(cloneDeep(items[index]))
// if(settings)
// this.settingsSaved(settings)
},
},
watch: {
initData(val) {
this.loadData(val)
},
},
mounted() {
this.initData && this.loadData(this.initData)
},
}
</script>

View File

@ -3,7 +3,7 @@
<div ref="list" :class="['a-select-file-list', listClass]"> <div ref="list" :class="['a-select-file-list', listClass]">
<!-- upload --> <!-- upload -->
<form ref="uploadForm" class="flex-column" v-if="state == STATE.DEFAULT"> <form ref="uploadForm" class="flex-column" v-if="state == STATE.DEFAULT">
<div class="field flex-grow-1" v-if="!uploadFile"> <div class="field flex-grow-1">
<label class="label">{{ uploadLabel }}</label> <label class="label">{{ uploadLabel }}</label>
<input type="file" ref="uploadFile" :name="uploadFieldName" @change="onSubmit"/> <input type="file" ref="uploadFile" :name="uploadFieldName" @change="onSubmit"/>
</div> </div>

View File

@ -35,7 +35,7 @@
</section> </section>
<section v-show="page == Page.List" class="panel"> <section v-show="page == Page.List" class="panel">
<a-rows :set="set" :columns="columns" :labels="labels" <a-rows :set="set" :columns="columns" :labels="initData.fields"
:allow-create="true" :allow-create="true"
:orderable="true" @move="listItemMove" @colmove="columnMove" :orderable="true" @move="listItemMove" @colmove="columnMove"
@cell="onCellEvent"> @cell="onCellEvent">
@ -49,8 +49,8 @@
<td class="align-right pr-0"> <td class="align-right pr-0">
<button type="button" class="button square" <button type="button" class="button square"
@click.stop="items.splice(data.row,1)" @click.stop="items.splice(data.row,1)"
:title="labels.remove_track" :title="labels.remove_item"
:aria-label="labels.remove_track"> :aria-label="labels.remove_item">
<span class="icon"><i class="fa fa-trash" /></span> <span class="icon"><i class="fa fa-trash" /></span>
</button> </button>
</td> </td>
@ -82,8 +82,8 @@
</button> </button>
<button type="button" class="button square is-primary p-2" v-if="page == Page.List" <button type="button" class="button square is-primary p-2" v-if="page == Page.List"
@click="this.set.push(new this.set.model())" @click="this.set.push(new this.set.model())"
:title="labels.add_track" :title="labels.add_item"
:aria-label="labels.add_track" :aria-label="labels.add_item"
> >
<span class="icon"><i class="fa fa-plus"/></span> <span class="icon"><i class="fa fa-plus"/></span>
</button> </button>
@ -99,7 +99,7 @@
<table class="table is-bordered" <table class="table is-bordered"
style="vertical-align: middle"> style="vertical-align: middle">
<tr> <tr>
<a-row :columns="columns" :item="labels" <a-row :columns="columns" :item="initData.fields"
@move="formatMove" :orderable="true"> @move="formatMove" :orderable="true">
<template v-slot:cell-after="{cell}"> <template v-slot:cell-after="{cell}">
<td style="cursor:pointer;" v-if="cell.col < columns.length-1"> <td style="cursor:pointer;" v-if="cell.col < columns.length-1">
@ -149,8 +149,7 @@
</template> </template>
<script> <script>
import {dropRightWhile, cloneDeep, isEqual} from 'lodash' import {dropRightWhile, cloneDeep, isEqual} from 'lodash'
import {Set} from '../model' import Model, {Set} from '../model'
import Track from '../track'
import AActionButton from './AActionButton' import AActionButton from './AActionButton'
import ARow from './ARow' import ARow from './ARow'
@ -165,6 +164,7 @@ export const Page = {
export default { export default {
components: { AActionButton, ARow, ARows, AModal }, components: { AActionButton, ARow, ARows, AModal },
props: { props: {
///! initial data as: {items: [], fields: {column_name: label, settings: {}}
initData: Object, initData: Object,
dataPrefix: String, dataPrefix: String,
labels: Object, labels: Object,
@ -182,7 +182,7 @@ export default {
return { return {
Page: Page, Page: Page,
page: Page.Text, page: Page.Text,
set: new Set(Track), set: new Set(Model),
extraData: {}, extraData: {},
settings, settings,
savedSettings: cloneDeep(settings), savedSettings: cloneDeep(settings),

View File

@ -7,15 +7,16 @@ import AList from './AList'
import APage from './APage' import APage from './APage'
import APlayer from './APlayer' import APlayer from './APlayer'
import APlaylist from './APlaylist' import APlaylist from './APlaylist'
import ATracklistEditor from './ATracklistEditor'
import AProgress from './AProgress' import AProgress from './AProgress'
import ASoundItem from './ASoundItem' import ASoundItem from './ASoundItem'
import ASwitch from './ASwitch' import ASwitch from './ASwitch'
import AStatistics from './AStatistics'
import AStreamer from './AStreamer'
import AModal from "./AModal" import AModal from "./AModal"
import ASelectFile from "./ASelectFile" import ASelectFile from "./ASelectFile"
import AStatistics from './AStatistics'
import AStreamer from './AStreamer'
import ATracklistEditor from './ATracklistEditor'
import APlaylistEditor from './APlaylistEditor'
/** /**
* Core components * Core components
@ -35,5 +36,5 @@ export const admin = {
export const dashboard = { export const dashboard = {
...base, ...base,
AActionButton, ASelectFile, AModal, ATracklistEditor, AActionButton, ASelectFile, AModal, ATracklistEditor, APlaylistEditor
} }

View File

@ -8,13 +8,11 @@ const DashboardApp = {
...App, ...App,
components: {...App.components, ...components}, components: {...App.components, ...components},
/*
data() { data() {
return { return {
editPageContent: null, modalItem: null,
} }
}, },
*/
methods: { methods: {
...App.methods, ...App.methods,

View File

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