create EpisodeSound & adapt; update list editors

This commit is contained in:
bkfox
2024-03-26 17:43:04 +01:00
parent bda4efe336
commit 78a8478da8
36 changed files with 784 additions and 728 deletions

View File

@ -1,5 +1,5 @@
<template>
<component :is="tag" @click.capture.stop="call" type="button" :class="buttonClass">
<component :is="tag" @click.capture.stop="call" type="button" :class="[buttonClass, this.promise && 'blink' || '']">
<span v-if="promise && runIcon">
<i :class="runIcon"></i>
</span>

View File

@ -29,7 +29,7 @@
import {getCsrf} from "../model"
export default {
emit: ["fileChange", "load"],
emit: ["fileChange", "load", "abort", "error"],
props: {
url: { type: String },
@ -71,9 +71,9 @@ export default {
const req = new XMLHttpRequest()
req.open("POST", this.url)
req.upload.addEventListener("progress", (e) => this.onUploadProgress(e))
req.addEventListener("load", (e) => this.onUploadDone(e))
req.addEventListener("abort", (e) => this.onUploadDone(e))
req.addEventListener("error", (e) => this.onUploadDone(e))
req.addEventListener("load", (e) => this.onUploadDone(e, 'load'))
req.addEventListener("abort", (e) => this.onUploadDone(e, 'abort'))
req.addEventListener("error", (e) => this.onUploadDone(e, 'error'))
const formData = new FormData(this.$refs.form);
formData.append('csrfmiddlewaretoken', getCsrf())
@ -87,8 +87,8 @@ export default {
this.total = event.total
},
onUploadDone(event) {
this.$emit("load", event)
onUploadDone(event, eventName) {
this.$emit(eventName, event)
this._resetUpload(this.STATE.DEFAULT, true)
},

View File

@ -6,6 +6,7 @@
<div class="modal-card-title">
<slot name="title">{{ title }}</slot>
</div>
<slot name="bar"></slot>
<button type="button" class="delete square" aria-label="close" @click="close">
<span class="icon">
<i class="fa fa-close"></i>

View File

@ -1,63 +1,100 @@
<template>
<div class="a-select-file">
<div ref="list" :class="['a-select-file-list', listClass]">
<!-- upload -->
<form ref="uploadForm" class="flex-column" v-if="state == STATE.DEFAULT">
<div class="field flex-grow-1">
<label class="label">{{ uploadLabel }}</label>
<input type="file" ref="uploadFile" :name="uploadFieldName" @change="onSubmit"/>
</div>
<div class="flex-grow-1">
<slot name="upload-form"></slot>
</div>
</form>
<div class="flex-column" v-else>
<slot name="upload-preview" :upload="upload"></slot>
<div class="flex-row">
<progress :max="upload.total" :value="upload.loaded"/>
<button type="button" class="button small square ml-2" @click="uploadAbort">
<span class="icon small">
<i class="fa fa-close"></i>
</span>
</button>
<a-modal ref="modal" :title="title">
<template #bar>
<button type="button" class="button small mr-3" v-if="panel == LIST"
@click="showPanel(UPLOAD)">
<span class="icon">
<i class="fa fa-upload"></i>
</span>
<span>{{ labels.upload }}</span>
</button>
<button type="button" class="button small mr-3" v-else
@click="showPanel(LIST)">
<span class="icon">
<i class="fa fa-list"></i>
</span>
<span>{{ labels.list }}</span>
</button>
</template>
<template #default>
<div class="a-select-file">
<a-file-upload ref="upload" v-if="panel == UPLOAD"
:url="uploadUrl"
:label="uploadLabel" :field-name="uploadFieldName"
@load="uploadDone">
<template #form="data">
<slot name="upload-form" v-bind="data"></slot>
</template>
<template #preview="data">
<slot name="upload-preview" v-bind="data"></slot>
</template>
</a-file-upload>
<div ref="list" v-show="panel == LIST"
:class="['a-select-file-list', listClass]">
<!-- tiles -->
<div v-if="prevUrl">
<a href="#" @click="load(prevUrl)">
{{ labels.show_previous }}
</a>
</div>
<template v-for="item in items" v-bind:key="item.id">
<div :class="['file-preview', this.item && item.id == this.item.id && 'active']" @click="select(item)">
<slot :item="item" :load="load" :lastUrl="lastUrl"></slot>
<a-action-button v-if="deleteUrl"
class="has-text-danger small float-right"
icon="fa fa-trash"
:confirm="labels.confirm_delete"
method="DELETE"
:url="deleteUrl.replace('123', item.id)"
@done="load(lastUrl)">
</a-action-button>
</div>
</template>
<div v-if="nextUrl">
<a href="#" @click="load(nextUrl)">
{{ labels.show_next }}
</a>
</div>
</div>
</div>
<!-- tiles -->
<div v-if="prevUrl">
<a href="#" @click="load(prevUrl)">
{{ prevLabel }}
</a>
</div>
<template v-for="item in items" v-bind:key="item.id">
<div :class="['file-preview', this.item && item.id == this.item.id && 'active']" @click="select(item)">
<slot :item="item" :load="load" :lastUrl="lastUrl"></slot>
</div>
</template>
<div v-if="nextUrl">
<a href="#" @click="load(nextUrl)">
{{ nextLabel }}
</a>
</div>
</div>
<div class="a-select-footer">
<slot name="footer" :item="item" :items="items"></slot>
</div>
</div>
</template>
<template #footer>
<slot name="footer" :item="item">
<span class="mr-3" v-if="item">{{ item.name }}</span>
</slot>
<button type="button" v-if="panel == LIST" class="button align-right"
@click="selected">
{{ labels.select_file }}
</button>
</template>
</a-modal>
</template>
<script>
import {getCsrf} from "../model"
import AModal from "./AModal"
import AActionButton from "./AActionButton"
import AFileUpload from "./AFileUpload"
export default {
emit: ["select"],
components: {AActionButton, AFileUpload, AModal},
props: {
name: { type: String },
title: { type: String },
labels: Object,
listClass: {type: String, default: ""},
prevLabel: { type: String, default: "Prev" },
nextLabel: { type: String, default: "Next" },
// List url
listUrl: { type: String },
// URL to delete an item, where "123" is replaced by
// the item id.
deleteUrl: {type: String },
uploadUrl: { type: String },
uploadFieldName: { type: String, default: "file" },
uploadLabel: { type: String, default: "Upload a file" },
@ -65,91 +102,63 @@ export default {
data() {
return {
STATE: {
DEFAULT: 0,
UPLOADING: 1,
},
state: 0,
LIST: 0,
UPLOAD: 1,
panel: 0,
item: null,
items: [],
nextUrl: "",
prevUrl: "",
lastUrl: "",
upload: {},
}
},
methods: {
open() {
this.$refs.modal.open()
},
close() {
this.$refs.modal.close()
},
showPanel(panel) {
this.panel = panel
},
load(url) {
fetch(url || this.listUrl).then(
return fetch(url || this.listUrl).then(
response => response.ok ? response.json() : Promise.reject(response)
).then(data => {
this.lastUrl = url
this.nextUrl = data.next
this.prevUrl = data.previous
this.items = data.results
this.showPanel(this.LIST)
this.$forceUpdate()
this.$refs.list.scroll(0, 0)
return this.items
})
},
//! Select an item
select(item) {
this.item = item;
},
// ---- upload
uploadAbort() {
this.upload.request && this.upload.request.abort()
//! User click on select button (confirm selection)
selected() {
this.$emit("select", this.item)
this.close()
},
onSubmit() {
const [file] = this.$refs.uploadFile.files
if(!file)
return
this._setUploadFile(file)
const req = new XMLHttpRequest()
req.open("POST", this.uploadUrl || this.listUrl)
req.upload.addEventListener("progress", (e) => this.onUploadProgress(e))
req.addEventListener("load", (e) => this.onUploadDone(e, true))
req.addEventListener("abort", (e) => this.onUploadDone(e))
req.addEventListener("error", (e) => this.onUploadDone(e))
const formData = new FormData(this.$refs.uploadForm);
formData.append('csrfmiddlewaretoken', getCsrf())
req.send(formData)
this._resetUpload(this.STATE.UPLOADING, false, req)
uploadDone(reload=false) {
reload && this.load().then(items => {
this.item = items[0]
})
},
onUploadProgress(event) {
this.upload.loaded = event.loaded
this.upload.total = event.total
},
onUploadDone(reload=false) {
this._resetUpload(this.STATE.DEFAULT, true)
reload && this.load()
},
_setUploadFile(file) {
this.upload.file = file
this.upload.fileURL = file && URL.createObjectURL(file)
},
_resetUpload(state, resetFile=false, request=null) {
this.state = state
this.upload.loaded = 0
this.upload.total = 0
this.upload.request = request
if(resetFile)
this.upload.file = null
}
},
mounted() {

View File

@ -1,27 +1,25 @@
<template>
<div class="a-playlist-editor">
<a-modal ref="modal" :title="labels && labels.add_sound">
<template #default>
<a-file-upload ref="file-upload" :url="soundUploadUrl" :label="labels.select_file" submitLabel="" @load="uploadDone"
>
<template #preview="{upload}">
<slot name="upload-preview" :upload="upload"></slot>
</template>
<template #form>
<slot name="upload-form"></slot>
</template>
</a-file-upload>
<a-select-file ref="select-file"
:title="labels && labels.add_sound"
:labels="labels"
:list-url="soundListUrl"
:deleteUrl="soundDeleteUrl"
:uploadUrl="soundUploadUrl"
:uploadLabel="labels.select_file"
@select="selected"
>
<template #upload-preview="{upload}">
<slot name="upload-preview" :upload="upload"></slot>
</template>
<template #footer>
<button type="button" class="button"
@click.stop="$refs['file-upload'].submit()">
<span class="icon">
<i class="fa fa-upload"></i>
</span>
<span>{{ labels.submit }}</span>
</button>
<template #upload-form>
<slot name="upload-form"></slot>
</template>
</a-modal>
<template #default="{item}">
<audio controls :src="item.url"></audio>
<label class="label small flex-grow-1">{{ item.name }}</label>
</template>
</a-select-file>
<slot name="top" :set="set" :items="set.items"></slot>
<a-rows :set="set" :columns="allColumns"
@ -31,6 +29,11 @@
v-slot:[slot]="data">
<slot v-if="name != 'row-tail'" :name="name" v-bind="data"/>
</template>
<template #row-sound="{item}">
<label>{{ item.data.name }}</label><br>
<audio controls :src="item.data.url"/>
</template>
</a-rows>
<div class="flex-row">
@ -45,7 +48,7 @@
<span class="icon"><i class="fa fa-rotate" /></span>
</button>
<button type="button" class="button square is-primary p-2"
@click="$refs.modal.open()"
@click="$refs['select-file'].open()"
:title="labels.add_sound"
:aria-label="labels.add_sound"
>
@ -61,25 +64,27 @@
import {cloneDeep} from 'lodash'
import Model, {Set} from '../model'
// import AActionButton from './AActionButton'
import ARows from './ARows'
import AModal from "./AModal"
import AFileUpload from "./AFileUpload"
//import AFileUpload from "./AFileUpload"
import ASelectFile from "./ASelectFile"
export default {
components: {ARows, AModal, AFileUpload},
components: {ARows, ASelectFile},
props: {
// default values of items
itemDefaults: Object,
// initial datas
initData: Object,
dataPrefix: String,
labels: Object,
settingsUrl: String,
soundListUrl: String,
soundUploadUrl: String,
player: Object,
soundDeleteUrl: String,
columns: {
type: Array,
default: () => ['name', "type", 'is_public', 'is_downloadable']
default: () => ['name', "broadcast"]
},
},
@ -95,7 +100,7 @@ export default {
},
allColumns() {
return [...this.columns, "delete"]
return ["sound", ...this.columns, "delete"]
},
allColumnsLabels() {
@ -131,17 +136,18 @@ export default {
// this.settingsSaved(settings)
},
uploadDone(event) {
const req = event.target
if(req.status == 201) {
const item = JSON.parse(req.response)
this.set.push(item)
this.$refs.modal.close()
selected(item) {
const data = {
...this.itemDefaults,
"sound": item.id,
"name": item.name,
"url": item.url,
"broadcast": item.broadcast,
}
this.set.push(data)
},
},
watch: {
initData(val) {
this.loadData(val)

View File

@ -27,7 +27,7 @@
</div>
</div>
</div>
<slot name="top" :set="set" :columns="columns" :items="items"/>
<slot name="top" :set="set" :columns="allColumns" :items="items"/>
<section v-show="page == Page.Text" class="panel">
<textarea ref="textarea" class="is-fullwidth is-size-6" rows="20"
@change="updateList"
@ -35,7 +35,7 @@
</section>
<section v-show="page == Page.List" class="panel">
<a-rows :set="set" :columns="columns" :labels="initData.fields"
<a-rows :set="set" :columns="allColumns" :labels="initData.fields"
:orderable="true" @move="listItemMove" @colmove="columnMove"
@cell="onCellEvent">
<template v-for="[name,slot] of rowsSlots" :key="slot"
@ -98,10 +98,10 @@
<table class="table is-bordered"
style="vertical-align: middle">
<tr>
<a-row :columns="columns" :item="initData.fields"
<a-row :columns="allColumns" :item="initData.fields"
@move="formatMove" :orderable="true">
<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 < allColumns.length-1">
<span class="icon" @click="formatMove({from: cell.col, to: cell.col+1})"
><i class="fa fa-left-right"/>
</span>
@ -143,7 +143,7 @@
</div>
</template>
</a-modal>
<slot name="bottom" :set="set" :columns="columns" :items="items"/>
<slot name="bottom" :set="set" :columns="allColumns" :items="items"/>
</div>
</template>
<script>
@ -175,7 +175,7 @@ export default {
data() {
const settings = {
tracklist_editor_columns: this.defaultColumns,
tracklist_editor_columns: this.columns,
tracklist_editor_sep: ' -- ',
}
return {
@ -204,7 +204,7 @@ export default {
get() { return this.settings.tracklist_editor_sep }
},
columns: {
allColumns: {
set(value) {
var cols = value.filter(x => x in this.defaultColumns)
var left = this.defaultColumns.filter(x => !(x in cols))
@ -236,7 +236,7 @@ export default {
},
formatMove({from, to}) {
const value = this.columns[from]
const value = this.allColumns[from]
this.settings.tracklist_editor_columns.splice(from, 1)
this.settings.tracklist_editor_columns.splice(to, 0, value)
if(this.page == Page.Text)
@ -246,9 +246,9 @@ export default {
},
columnMove({from, to}) {
const value = this.columns[from]
this.columns.splice(from, 1)
this.columns.splice(to, 0, value)
const value = this.allColumns[from]
this.allColumns.splice(from, 1)
this.allColumns.splice(to, 0, value)
this.updateInput()
},
@ -281,10 +281,10 @@ export default {
var lineBits = line.split(this.separator)
var item = {}
for(var col in this.columns) {
for(var col in this.allColumns) {
if(col >= lineBits.length)
break
const attr = this.columns[col]
const attr = this.allColumns[col]
item[attr] = lineBits[col].trim()
}
item && items.push(item)
@ -302,7 +302,7 @@ export default {
if(!item)
continue
var line = []
for(var col of this.columns)
for(var col of this.allColumns)
line.push(item.data[col] || '')
line = dropRightWhile(line, x => !x || !('' + x).trim())
line = line.join(sep).trimRight()

View File

@ -17,13 +17,12 @@ const DashboardApp = {
methods: {
...App.methods,
fileSelected(select, cover, input, modal) {
console.log("file!")
fileSelected(select, input, preview) {
const item = this.$refs[select].item
if(item) {
this.$refs[cover].src = item.file
this.$refs[input].value = item.id
modal && this.$refs[modal].close()
if(preview)
preview.src = item.file
}
},
}

View File

@ -2,8 +2,11 @@ import Model from './model';
export default class Sound extends Model {
constructor({sound={}, ...data}={}, options={}) {
// flatten EpisodeSound and sound data
super({...sound, ...data}, options)
}
get name() { return this.data.name }
get src() { return this.data.url }
static getId(data) { return data.pk }
}