rename playlist-editor to tracklist-editor; refactor player

This commit is contained in:
bkfox 2024-03-20 01:42:01 +01:00
parent 3ad886764c
commit d293eb4a00
20 changed files with 214 additions and 149 deletions

View File

@ -44,7 +44,7 @@ class EpisodeForm(PageForm):
class Meta: class Meta:
model = models.Episode model = models.Episode
fields = ["content"] fields = PageForm.Meta.fields
def save(self, commit=True): def save(self, commit=True):
file_obj = self.cleaned_data["new_podcast"] file_obj = self.cleaned_data["new_podcast"]

View File

@ -32,10 +32,23 @@ class Episode(Page):
@cached_property @cached_property
def podcasts(self): def podcasts(self):
"""Return serialized data about podcasts.""" """Return serialized data about podcasts."""
from .sound import Sound
from ..serializers import PodcastSerializer from ..serializers import PodcastSerializer
podcasts = [PodcastSerializer(s).data for s in self.sound_set.public().order_by("type")] query = self.sound_set.public().order_by("type")
return self._to_podcasts(query, PodcastSerializer)
@cached_property
def sounds(self):
"""Return serialized data about all related sounds."""
from ..serializers import SoundSerializer
query = self.sound_set.order_by("type")
return self._to_podcasts(query, SoundSerializer)
def _to_podcasts(self, items, serializer_class):
from .sound import Sound
podcasts = [serializer_class(s).data for s in items]
if self.cover: if self.cover:
options = {"size": (128, 128), "crop": "scale"} options = {"size": (128, 128), "crop": "scale"}
cover = get_thumbnailer(self.cover).get_thumbnail(options).url cover = get_thumbnailer(self.cover).get_thumbnail(options).url

View File

@ -14,5 +14,5 @@ class UserSettings(models.Model):
verbose_name=_("User"), verbose_name=_("User"),
related_name="aircox_settings", related_name="aircox_settings",
) )
playlist_editor_columns = models.JSONField(_("Playlist Editor Columns")) tracklist_editor_columns = models.JSONField(_("Playlist Editor Columns"))
playlist_editor_sep = models.CharField(_("Playlist Editor Separator"), max_length=16) tracklist_editor_sep = models.CharField(_("Playlist Editor Separator"), max_length=16)

View File

@ -35,10 +35,10 @@ class TrackSerializer(TaggitSerializer, serializers.ModelSerializer):
class UserSettingsSerializer(serializers.ModelSerializer): class UserSettingsSerializer(serializers.ModelSerializer):
# TODO: validate fields values (playlist_editor_columns at least) # TODO: validate fields values (tracklist_editor_columns at least)
class Meta: class Meta:
model = UserSettings model = UserSettings
fields = ("playlist_editor_columns", "playlist_editor_sep") fields = ("tracklist_editor_columns", "tracklist_editor_sep")
def create(self, validated_data): def create(self, validated_data):
user = self.context.get("user") user = self.context.get("user")

View File

@ -7,6 +7,7 @@ __all__ = ("SoundSerializer", "PodcastSerializer")
class SoundSerializer(serializers.ModelSerializer): class SoundSerializer(serializers.ModelSerializer):
file = serializers.FileField(use_url=False) file = serializers.FileField(use_url=False)
type_display = serializers.SerializerMethodField()
class Meta: class Meta:
model = Sound model = Sound
@ -16,14 +17,19 @@ class SoundSerializer(serializers.ModelSerializer):
"program", "program",
"episode", "episode",
"type", "type",
"type_display",
"file", "file",
"duration", "duration",
"mtime", "mtime",
"is_good_quality", "is_good_quality",
"is_public", "is_public",
"is_downloadable",
"url", "url",
] ]
def get_type_display(self, obj):
return obj.get_type_display()
class PodcastSerializer(serializers.ModelSerializer): class PodcastSerializer(serializers.ModelSerializer):
# serializers.HyperlinkedIdentityField(view_name='sound', format='html') # serializers.HyperlinkedIdentityField(view_name='sound', format='html')

View File

@ -533,7 +533,7 @@
overflow: hidden; overflow: hidden;
} }
.a-playlist-editor .dropdown { .a-tracklist-editor .dropdown {
display: unset !important; display: unset !important;
} }

View File

@ -533,7 +533,7 @@
overflow: hidden; overflow: hidden;
} }
.a-playlist-editor .dropdown { .a-tracklist-editor .dropdown {
display: unset !important; display: unset !important;
} }

File diff suppressed because one or more lines are too long

View File

@ -7,7 +7,7 @@
<div id="inline-tracks" class="box mb-5"> <div id="inline-tracks" class="box mb-5">
{{ admin_formset.non_form_errors }} {{ admin_formset.non_form_errors }}
<a-playlist-editor <a-tracklist-editor
:labels="{% track_inline_labels %}" :labels="{% track_inline_labels %}"
:init-data="{% track_inline_data formset=formset %}" :init-data="{% track_inline_data formset=formset %}"
settings-url="{% url "api:user-settings" %}" settings-url="{% url "api:user-settings" %}"
@ -79,7 +79,7 @@
</template> </template>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</a-playlist-editor> </a-tracklist-editor>
</div> </div>
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}

View File

@ -2,7 +2,42 @@
{% load static i18n humanize honeypot aircox %} {% load static i18n humanize honeypot aircox %}
{% block page_form %} {% block page_form %}
{{ block.super }} <a-episode :page="{title: &quot;{{ object.title }}&quot;, podcasts: {{ object.sounds|json }}}">
<hr/> <template v-slot="{podcasts,page}">
{% include "./widgets/playlist_editor.html" with formset=playlist_formset %} {{ block.super }}
<hr/>
{% include "./widgets/tracklist_editor.html" with formset=playlist_formset %}
<hr/>
<section class="container">
<h3 class="title">{% translate "Sound files" %}</h3>
<a-playlist v-if="page" :set="podcasts"
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>
</template>
</a-episode>
{% endblock %} {% endblock %}

View File

@ -39,7 +39,7 @@
icon="fa fa-trash" icon="fa fa-trash"
confirm="{% translate "Are you sure you want to remove this item from server?" %}" confirm="{% translate "Are you sure you want to remove this item from server?" %}"
method="DELETE" method="DELETE"
:url="'{% url "api:image-detail" pk="123" %}'.replace('123', item.id)"q :url="'{% url "api:image-detail" pk="123" %}'.replace('123', item.id)"
@done="load(lastUrl)"> @done="load(lastUrl)">
</a-action-button> </a-action-button>
</div> </div>

View File

@ -20,6 +20,7 @@ The audio player
<a-player ref="player" <a-player ref="player"
:live-args="{% player_live_attr %}" :live-args="{% player_live_attr %}"
:playlists="{pin: ['{% translate "Bookmarks" %}', 'fa fa-star'], queue: ['{% translate 'Playlist' %}', 'fa fa-list']}"
button-title="{% translate "Play or pause audio" %}"> button-title="{% translate "Play or pause audio" %}">
<template v-slot:content="{ loaded, live, current }"> <template v-slot:content="{ loaded, live, current }">
<h4 v-if="loaded" class="title"> <h4 v-if="loaded" class="title">

View File

@ -10,7 +10,7 @@ Context:
{{ formset.non_form_errors }} {{ formset.non_form_errors }}
<!-- formset.management_form --> <!-- formset.management_form -->
<a-playlist-editor <a-tracklist-editor
:labels="{% track_inline_labels %}" :labels="{% track_inline_labels %}"
:init-data="{% track_inline_data formset=formset %}" :init-data="{% track_inline_data formset=formset %}"
:default-columns="[{% for f in fields %}{% if f != "position" %}'{{ f }}',{% endif %}{% endfor %}]" :default-columns="[{% for f in fields %}{% if f != "position" %}'{{ f }}',{% endif %}{% endfor %}]"
@ -74,6 +74,6 @@ Context:
</template> </template>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</a-playlist-editor> </a-tracklist-editor>
</div> </div>
{% endwith %} {% endwith %}

View File

@ -52,7 +52,13 @@ class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
form_class = EpisodeForm form_class = EpisodeForm
template_name = "aircox/episode_form.html" template_name = "aircox/episode_form.html"
playlist_fields = ("position", "artist", "title", "tags", "album", "info") playlist_fields = (
"position",
"artist",
"title",
"tags",
"album",
)
"""Playlist editor's ordered fields.""" """Playlist editor's ordered fields."""
def test_func(self): def test_func(self):

View File

@ -1,39 +1,29 @@
<template> <template>
<div class="a-player"> <div class="a-player">
<div :class="['a-player-panels', panel ? 'is-open' : '']"> <div :class="['a-player-panels', panel ? 'is-open' : '']">
<APlaylist ref="pin" class="a-player-panel a-playlist" v-show="panel == 'pin' && sets.pin.length" <template v-for="(info, key) in playlists" v-bind:key="key">
name="Pinned" <APlaylist
:actions="['page']" :ref="key" class="a-player-panel a-playlist"
:editable="true" :player="self" :set="sets.pin" @select="togglePlay('pin', $event.index)" v-show="panel == key && sets[key].length"
listClass="menu-list" itemClass="menu-item"> :actions="['page', key != 'pin' && 'pin' || '']"
<template v-slot:header=""> :editable="true" :player="self" :set="sets[key]"
<div class="title is-flex-grow-1"> @select="togglePlay(key, $event.index)"
<span class="icon"><span class="fa fa-star"></span></span> listClass="menu-list" itemClass="menu-item">
Pinned <template v-slot:header="">
</div> <div class="title is-flex-grow-1">
<button class="action button no-border"> <span class="icon">
<span class="icon" @click.stop="togglePanel()"> <i :class="info[1]"></i>
<i class="fa fa-close"></i> </span>
</span> {{ info[0] }}
</button> </div>
</template> <button class="action button no-border">
</APlaylist> <span class="icon" @click.stop="togglePanel()">
<APlaylist ref="queue" class="a-player-panel a-playlist" v-show="panel == 'queue' && sets.queue.length" <i class="fa fa-close"></i>
:actions="['page']" </span>
:editable="true" :player="self" :set="sets.queue" @select="togglePlay('queue', $event.index)" </button>
listClass="menu-list" itemClass="menu-item"> </template>
<template v-slot:header=""> </APlaylist>
<div class="title is-flex-grow-1"> </template>
<span class="icon"><span class="fa fa-list"></span></span>
Playlist
</div>
<button class="action button no-border">
<span class="icon" @click.stop="togglePanel()">
<i class="fa fa-close"></i>
</span>
</button>
</template>
</APlaylist>
</div> </div>
<div class="a-player-progress" v-if="loaded && duration"> <div class="a-player-progress" v-if="loaded && duration">
@ -59,18 +49,18 @@
<span class="fa fa-circle"></span> <span class="fa fa-circle"></span>
</span> </span>
</button> </button>
<button ref="pinPlaylistButton" :class="playlistButtonClass('pin')" <template v-if="sets">
@click="togglePanel('pin')" v-show="sets.pin.length"> <template v-for="(info, key) in playlists" v-bind:key="key">
<span class="is-size-6" v-if="sets.pin.length"> <button :class="playlistButtonClass(key)"
{{ sets.pin.length }}</span> @click="togglePanel(key)"
<span class="icon"><span class="fa fa-star"></span></span> v-show="sets[key] && sets[key].length">
</button> <span class="is-size-6">{{ sets[key] && sets[key].length }}</span>
<button :class="playlistButtonClass('queue')" <span class="icon">
@click="togglePanel('queue')" v-show="sets.queue.length"> <i :class="info[1]"></i>
<span class="is-size-6" v-if="sets.queue.length"> </span>
{{ sets.queue.length }}</span> </button>
<span class="icon"><span class="fa fa-list"></span></span> </template>
</button> </template>
</div> </div>
</div> </div>
</template> </template>
@ -108,6 +98,11 @@ export default {
let live = this.liveArgs ? reactive(new Live(this.liveArgs)) : null; let live = this.liveArgs ? reactive(new Live(this.liveArgs)) : null;
live && live.refresh(); live && live.refresh();
const sets = {}
for(const key in this.playlists)
sets[key] = Set.storeLoad(Sound, 'playlist.' + key,
{max: 30, unique: true})
return { return {
audio, duration: 0, currentTime: 0, state: State.paused, audio, duration: 0, currentTime: 0, state: State.paused,
live, live,
@ -119,16 +114,15 @@ export default {
//! current playing playlist name //! current playing playlist name
playlistName: null, playlistName: null,
//! players' playlists' sets //! players' playlists' sets
sets: { sets,
queue: Set.storeLoad(Sound, "playlist.queue", { max: 30, unique: true }),
pin: Set.storeLoad(Sound, "player.pin", { max: 30, unique: true }),
}
} }
}, },
props: { props: {
buttonTitle: String, buttonTitle: String,
liveArgs: Object, liveArgs: Object,
///! dict of {'slug': ['Label', 'icon']}
playlists: Object,
}, },
computed: { computed: {
@ -138,7 +132,7 @@ export default {
loading() { return this.state == State.loading; }, loading() { return this.state == State.loading; },
playlist() { playlist() {
return this.playlistName ? this.$refs[this.playlistName] : null; return this.playlistName ? this.$refs[this.playlistName][0] : null;
}, },
current() { current() {
@ -178,8 +172,8 @@ export default {
_setPlaylist(playlist) { _setPlaylist(playlist) {
this.playlistName = playlist; this.playlistName = playlist;
for(var p in this.sets) for(var p in this.sets)
if(p != playlist) if(p != playlist && this.$refs[p])
this.$refs[p].unselect(); this.$refs[p][0].unselect();
}, },
/// Load a sound from playlist or live /// Load a sound from playlist or live
@ -188,7 +182,7 @@ export default {
// from playlist // from playlist
if(playlist !== null && index != -1) { if(playlist !== null && index != -1) {
let item = this.$refs[playlist].get(index); let item = this.$refs[playlist][0].get(index);
if(!item) if(!item)
throw `No sound at index ${index} for playlist ${playlist}`; throw `No sound at index ${index} for playlist ${playlist}`;
this.loaded = item this.loaded = item
@ -232,7 +226,7 @@ export default {
/// Push and play items /// Push and play items
playItems(playlist, ...items) { playItems(playlist, ...items) {
let index = this.push(playlist, ...items); let index = this.push(playlist, ...items);
this.$refs[playlist].selectedIndex = index; this.$refs[playlist][0].selectedIndex = index;
this.play(playlist, index); this.play(playlist, index);
}, },
@ -264,13 +258,14 @@ export default {
}, },
//! Pin/Unpin an item //! Pin/Unpin an item
togglePin(item) { togglePlaylist(playlist, item) {
let index = this.sets.pin.findIndex(item); const set = this.sets[playlist]
let index = set.findIndex(item);
if(index > -1) if(index > -1)
this.sets.pin.remove(index); set.remove(index);
else { else {
this.sets.pin.push(item); set.push(item);
this.$refs.pinPlaylistButton.focus(); // this.$refs.pinPlaylistButton.focus();
} }
}, },

View File

@ -8,7 +8,11 @@
:data="item" :index="index" :set="set" :player="player_" :data="item" :index="index" :set="set" :player="player_"
@togglePlay="togglePlay(index)" @togglePlay="togglePlay(index)"
:actions="actions"> :actions="actions">
<template v-slot:actions="{}"> <template #after-title="bindings">
<slot name="after-title" v-bind="bindings"></slot>
</template>
<template #actions="bindings">
<slot name="actions" v-bind="bindings"></slot>
<button class="button" v-if="editable" @click.stop="remove(index,true)"> <button class="button" v-if="editable" @click.stop="remove(index,true)">
<span class="icon is-small"><span class="fa fa-close"></span></span> <span class="icon is-small"><span class="fa fa-close"></span></span>
</button> </button>
@ -30,6 +34,7 @@ export default {
props: { props: {
actions: Array, actions: Array,
// FIXME: remove
name: String, name: String,
player: Object, player: Object,
editable: Boolean, editable: Boolean,

View File

@ -5,6 +5,8 @@
{{ name || item.name }} {{ name || item.name }}
</span> </span>
</slot> </slot>
<slot name="after-title" :player="player" :item="item" :loaded="loaded">
</slot>
<div class="button-group actions"> <div class="button-group actions">
<a class="button action" v-if="hasAction('page')" <a class="button action" v-if="hasAction('page')"
:href="item.data.page_url"> :href="item.data.page_url">
@ -12,13 +14,15 @@
<i class="fa fa-external-link"></i> <i class="fa fa-external-link"></i>
</span> </span>
</a> </a>
<a class="button action" v-if="item.data.is_downloadable" <a class="button action"
v-if="hasAction('download') && item.data.is_downloadable"
:href="item.data.url" target="_blank"> :href="item.data.url" target="_blank">
<span class="icon is-small"> <span class="icon is-small">
<span class="fa fa-download"></span> <span class="fa fa-download"></span>
</span> </span>
</a> </a>
<button :class="['button action', pinned ? 'selected' : 'not-selected']" v-if="player && player.sets.pin != $parent.set" @click.stop="player.togglePin(item)"> <button :class="['button action', pinned ? 'selected' : 'not-selected']"
v-if="hasAction('pin') && player && player.sets.pin != $parent.set" @click.stop="player.togglePlaylist('pin', item)">
<span class="icon is-small"> <span class="icon is-small">
<span class="fa fa-star"></span> <span class="fa fa-star"></span>
</span> </span>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="a-playlist-editor"> <div class="a-tracklist-editor">
<div class="flex-row"> <div class="flex-row">
<div class="flex-grow-1"> <div class="flex-grow-1">
<slot name="title" /> <slot name="title" />
@ -176,8 +176,8 @@ export default {
data() { data() {
const settings = { const settings = {
playlist_editor_columns: this.defaultColumns, tracklist_editor_columns: this.defaultColumns,
playlist_editor_sep: ' -- ', tracklist_editor_sep: ' -- ',
} }
return { return {
Page: Page, Page: Page,
@ -198,11 +198,11 @@ export default {
separator: { separator: {
set(value) { set(value) {
this.settings.playlist_editor_sep = value this.settings.tracklist_editor_sep = value
if(this.page == Page.List) if(this.page == Page.List)
this.updateInput() this.updateInput()
}, },
get() { return this.settings.playlist_editor_sep } get() { return this.settings.tracklist_editor_sep }
}, },
columns: { columns: {
@ -210,10 +210,10 @@ export default {
var cols = value.filter(x => x in this.defaultColumns) var cols = value.filter(x => x in this.defaultColumns)
var left = this.defaultColumns.filter(x => !(x in cols)) var left = this.defaultColumns.filter(x => !(x in cols))
value = cols.concat(left) value = cols.concat(left)
this.settings.playlist_editor_columns = value this.settings.tracklist_editor_columns = value
}, },
get() { get() {
return this.settings.playlist_editor_columns return this.settings.tracklist_editor_columns
} }
}, },
@ -238,8 +238,8 @@ export default {
formatMove({from, to}) { formatMove({from, to}) {
const value = this.columns[from] const value = this.columns[from]
this.settings.playlist_editor_columns.splice(from, 1) this.settings.tracklist_editor_columns.splice(from, 1)
this.settings.playlist_editor_columns.splice(to, 0, value) this.settings.tracklist_editor_columns.splice(to, 0, value)
if(this.page == Page.Text) if(this.page == Page.Text)
this.updateList() this.updateList()
else else

View File

@ -7,7 +7,7 @@ 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 APlaylistEditor from './APlaylistEditor' 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'
@ -30,10 +30,10 @@ export default base
export const admin = { export const admin = {
...base, ...base,
AStatistics, AStreamer, APlaylistEditor AStatistics, AStreamer, ATracklistEditor
} }
export const dashboard = { export const dashboard = {
...base, ...base,
AActionButton, ASelectFile, AModal, APlaylistEditor, AActionButton, ASelectFile, AModal, ATracklistEditor,
} }

View File

@ -710,7 +710,7 @@
/// ---- playlist editor /// ---- playlist editor
.a-playlist-editor { .a-tracklist-editor {
.dropdown { .dropdown {
display: unset !important; display: unset !important;
} }