#39: Playlist Editor #81

Merged
thomas merged 11 commits from dev-1.0-playlist-editor into develop-1.0 2023-01-25 11:17:05 +00:00
10 changed files with 62 additions and 31 deletions
Showing only changes of commit a53a37021c - Show all commits

View File

@ -70,6 +70,20 @@ class SoundAdmin(SortableAdminBase, admin.ModelAdmin):
if obj.type != Sound.TYPE_REMOVED else '' if obj.type != Sound.TYPE_REMOVED else ''
audio.short_description = _('Audio') audio.short_description = _('Audio')
def add_view(self, request, object_id, form_url='', context=None):
context = context or {}
context['init_app'] = True
context['init_el'] = '#inline-tracks'
context['track_timestamp'] = True
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'
context['track_timestamp'] = True
return super().change_view(request, object_id, form_url, context)
@admin.register(Track) @admin.register(Track)
class TrackAdmin(admin.ModelAdmin): class TrackAdmin(admin.ModelAdmin):

View File

@ -13,7 +13,7 @@ class TrackSerializer(TaggitSerializer, serializers.ModelSerializer):
class Meta: class Meta:
model = Track model = Track
fields = ('pk', 'artist', 'title', 'album', 'year', 'position', fields = ('pk', 'artist', 'title', 'album', 'year', 'position',
'info', 'tags', 'episode', 'sound') 'info', 'tags', 'episode', 'sound', 'timestamp')
class UserSettingsSerializer(serializers.ModelSerializer): class UserSettingsSerializer(serializers.ModelSerializer):

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,9 @@
<a-playlist-editor <a-playlist-editor
:labels="{% track_inline_labels %}" :labels="{% track_inline_labels %}"
:init-data="{% track_inline_data formset=formset %}" :init-data="{% track_inline_data formset=formset %}"
{% if not track_timestamp %}
:hide-columns="['timestamp']"
{% endif %}
settings-url="{% url "api:user-settings" %}" settings-url="{% url "api:user-settings" %}"
data-prefix="{{ formset.prefix }}-"> data-prefix="{{ formset.prefix }}-">
<template #title> <template #title>
@ -39,9 +42,9 @@
<input type="hidden" <input type="hidden"
:name="'{{ formset.prefix }}-' + row + '-position'" :name="'{{ formset.prefix }}-' + row + '-position'"
:value="row"/> :value="row"/>
<input t-if="item.id" type="hidden" <input t-if="item.data.id" type="hidden"
:name="'{{ formset.prefix }}-' + row + '-id'" :name="'{{ formset.prefix }}-' + row + '-id'"
:value="item.data.id"/> :value="item.data.id || item.id"/>
{% for field in admin_formset.fields %} {% for field in admin_formset.fields %}
{% if field.name != 'position' and field.widget.is_hidden %} {% if field.name != 'position' and field.widget.is_hidden %}
@ -54,7 +57,7 @@
</template> </template>
{% for field in admin_formset.fields %} {% for field in admin_formset.fields %}
{% 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.name }}="{item,col,row,value,attr,emit}"> <template v-slot:row-{{ field.name }}="{item,cell,value,attr,emit}">
<div class="field"> <div class="field">
{% if field.name in 'artist,title,album' %} {% if field.name in 'artist,title,album' %}
<a-autocomplete <a-autocomplete
@ -65,7 +68,7 @@
<input type="{{ widget.type }}" <input type="{{ widget.type }}"
:class="['input', item.error(attr) ? 'is-danger' : 'half-field']" :class="['input', item.error(attr) ? 'is-danger' : 'half-field']"
{% endif %} {% endif %}
:name="'{{ formset.prefix }}-' + row + '-{{ field.name }}'" :name="'{{ formset.prefix }}-' + cell.row + '-{{ field.name }}'"
v-model="item.data[attr]" v-model="item.data[attr]"
@change="emit('change', col)"/> @change="emit('change', col)"/>
{% if field.name not in 'artist,title,album' %} {% if field.name not in 'artist,title,album' %}

View File

@ -56,5 +56,8 @@ def do_track_inline_labels():
'save_settings': __('Save Settings'), 'save_settings': __('Save Settings'),
'discard_changes': __('Discard changes'), 'discard_changes': __('Discard changes'),
'columns': __('Columns'), 'columns': __('Columns'),
'add_track': __('Add a track'),
'remove_track': __('Remove'),
'timestamp': __('Timestamp'),
}) })

View File

@ -140,7 +140,6 @@ export default {
return this.labelField ? item && item[this.labelField] : item; return this.labelField ? item && item[this.labelField] : item;
}, },
hide() { hide() {
this.cursor = -1; this.cursor = -1;
this.selectedIndex = -1; this.selectedIndex = -1;

View File

@ -41,7 +41,19 @@
@cell="onCellEvent"> @cell="onCellEvent">
<template v-for="[name,slot] of rowsSlots" :key="slot" <template v-for="[name,slot] of rowsSlots" :key="slot"
v-slot:[slot]="data"> v-slot:[slot]="data">
<slot :name="name" v-bind="data"/> <slot v-if="name != 'row-tail'" :name="name" v-bind="data"/>
</template>
<template v-slot:row-tail="data">
<slot v-if="$slots['row-tail']" :name="row-tail" v-bind="data"/>
<td>
<a class="button is-danger is-outlined p-3 is-size-6"
@click="items.splice(data.row,1)"
:title="labels.remove_track"
:aria-label="labels.remove_track">
<span class="icon"><i class="fa fa-trash" /></span>
</a>
</td>
</template> </template>
</a-rows> </a-rows>
</section> </section>
@ -58,7 +70,7 @@
v-model="separator" @change="updateList()"/> v-model="separator" @change="updateList()"/>
</div> </div>
</div> </div>
<div class="field is-inline-block is-vcentered mr-5"> <div class="field is-inline-block is-vcentered mr-3">
<label class="label is-inline mr-2" <label class="label is-inline mr-2"
style="vertical-align: middle"> style="vertical-align: middle">
{{ labels.columns }}</label> {{ labels.columns }}</label>
@ -95,6 +107,11 @@
<span class="icon"><i class="fa fa-rotate" /></span> <span class="icon"><i class="fa fa-rotate" /></span>
<span>{{ labels.discard_changes }}</span> <span>{{ labels.discard_changes }}</span>
</a> </a>
<a class="button is-primary p-2 ml-2" t-if="page == page.List"
@click="this.set.push(new this.set.model())">
<span class="icon"><i class="fa fa-plus"/></span>
<span>{{ labels.add_track }}</span>
</a>
</div> </div>
</div> </div>
<slot name="bottom" :set="set" :columns="columns" :items="items"/> <slot name="bottom" :set="set" :columns="columns" :items="items"/>
@ -123,7 +140,7 @@ export default {
settingsUrl: String, settingsUrl: String,
defaultColumns: { defaultColumns: {
type: Array, type: Array,
default: () => ['artist', 'title', 'tags', 'album', 'year']}, default: () => ['artist', 'title', 'tags', 'album', 'year', 'timestamp']},
}, },
data() { data() {

View File

@ -12,6 +12,9 @@
:value="itemData && itemData[attr]"> :value="itemData && itemData[attr]">
{{ itemData && itemData[attr] }} {{ itemData && itemData[attr] }}
</slot> </slot>
<slot name="cell" :item="item" :cell="cells[col]"
:data="itemData" :attr="attr" :emit="cellEmit"
:value="itemData && itemData[attr]"/>
</component> </component>
<slot name="cell-after" :item="item" :col="col" :cell="cells[col]" <slot name="cell-after" :item="item" :col="col" :cell="cells[col]"
:attr="attr"/> :attr="attr"/>

View File

@ -31,14 +31,6 @@
</template> </template>
</a-row> </a-row>
</template> </template>
<template v-if="allowCreate">
<a-row :item="extraItem" :cell="{row:items.length, 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"/> <slot name="tail"/>
</tbody> </tbody>
</table> </table>
@ -85,13 +77,6 @@ const Component = {
}, },
methods: { methods: {
validateExtraCell() {
if(!this.allowCreate)
return
this.set.push(this.extraItem)
this.extraItem = new this.set.model()
},
onControlKey(event, cell) { onControlKey(event, cell) {
switch(event.key) { switch(event.key) {
case "ArrowUp": this.focus(-1, 0, cell) case "ArrowUp": this.focus(-1, 0, cell)

View File

@ -42,7 +42,7 @@ export default class Model {
} }
get errors() { get errors() {
return this.data.__errors__ return this.data && this.data.__errors__
} }
/** /**
@ -143,6 +143,13 @@ export default class Model {
return item === null ? item : new this(JSON.parse(item)); return item === null ? item : new this(JSON.parse(item));
} }
/**
* Return true if model instance has no data
*/
get isEmpty() {
return !this.data || Object.keys(this.data).findIndex(k => !!this.data[k] && this.data[k] !== 0) == -1
}
/** /**
* Return error for a specific attribute name if any * Return error for a specific attribute name if any
*/ */