upload selector improvements

This commit is contained in:
bkfox 2024-03-17 21:00:07 +01:00
parent de858f45e8
commit 024db5f307
9 changed files with 226 additions and 77 deletions

View File

@ -2777,7 +2777,7 @@ a.navbar-item:focus, a.navbar-item:focus-within, a.navbar-item:hover, a.navbar-i
#player .button, #player a.button, #player button.button, .ax .button, .ax a.button, .ax button.button {
font-size: 1rem;
display: inline-block;
padding: 0.4rem;
padding: 0.4em;
border: none;
justify-content: center;
text-align: center;
@ -2786,8 +2786,14 @@ a.navbar-item:focus, a.navbar-item:focus-within, a.navbar-item:hover, a.navbar-i
color: var(--button-fg);
background-color: var(--button-bg);
}
#player .button.smaller, #player a.button.smaller, #player button.button.smaller, .ax .button.smaller, .ax a.button.smaller, .ax button.button.smaller {
font-size: 0.8rem;
}
#player .button.small, #player a.button.small, #player button.button.small, .ax .button.small, .ax a.button.small, .ax button.button.small {
font-size: 0.6rem;
}
#player .button.square, #player a.button.square, #player button.button.square, .ax .button.square, .ax a.button.square, .ax button.button.square {
min-width: 2.5rem;
min-width: 2.5em;
}
#player .button.secondary, #player a.button.secondary, #player button.button.secondary, .ax .button.secondary, .ax a.button.secondary, .ax button.button.secondary {
background-color: var(--button-sec-bg);
@ -2799,10 +2805,10 @@ a.navbar-item:focus, a.navbar-item:focus-within, a.navbar-item:hover, a.navbar-i
vertical-align: middle;
}
#player .button .icon:not(:only-child):first-child, #player a.button .icon:not(:only-child):first-child, #player button.button .icon:not(:only-child):first-child, .ax .button .icon:not(:only-child):first-child, .ax a.button .icon:not(:only-child):first-child, .ax button.button .icon:not(:only-child):first-child {
margin: 0 0.6rem 0 0.2rem;
margin: 0 0.6em 0 0.2em;
}
#player .button .icon:not(:only-child):last-child, #player a.button .icon:not(:only-child):last-child, #player button.button .icon:not(:only-child):last-child, .ax .button .icon:not(:only-child):last-child, .ax a.button .icon:not(:only-child):last-child, .ax button.button .icon:not(:only-child):last-child {
margin: 0 0.6rem 0 0.2rem;
margin: 0 0.6em 0 0.2em;
}
#player .button:hover, #player a.button:hover, #player button.button:hover, .ax .button:hover, .ax a.button:hover, .ax button.button:hover {
color: var(--button-hv-fg);
@ -9553,6 +9559,22 @@ a.tag:hover {
color: var(--text-color-light);
}
.bigger {
font-size: 1.6rem !important;
}
.big {
font-size: 2rem !important;
}
.smaller {
font-size: 0.8rem !important;
}
.small {
font-size: 0.6rem !important;
}
.align-left {
text-align: left;
justify-content: left;
@ -9621,6 +9643,58 @@ a.tag:hover {
white-space: nowrap;
}
.height-1 {
height: 1em;
}
.height-2 {
height: 2em;
}
.height-3 {
height: 3em;
}
.height-4 {
height: 4em;
}
.height-5 {
height: 5em;
}
.height-6 {
height: 6em;
}
.height-7 {
height: 7em;
}
.height-8 {
height: 8em;
}
.height-9 {
height: 9em;
}
.height-10 {
height: 10em;
}
.height-15 {
height: 15em;
}
.height-20 {
height: 20em;
}
.height-25 {
height: 25em;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;

View File

@ -621,7 +621,7 @@
.button, a.button, button.button {
font-size: 1rem;
display: inline-block;
padding: 0.4rem;
padding: 0.4em;
border: none;
justify-content: center;
text-align: center;
@ -630,8 +630,14 @@
color: var(--button-fg);
background-color: var(--button-bg);
}
.button.smaller, a.button.smaller, button.button.smaller {
font-size: 0.8rem;
}
.button.small, a.button.small, button.button.small {
font-size: 0.6rem;
}
.button.square, a.button.square, button.button.square {
min-width: 2.5rem;
min-width: 2.5em;
}
.button.secondary, a.button.secondary, button.button.secondary {
background-color: var(--button-sec-bg);
@ -643,10 +649,10 @@
vertical-align: middle;
}
.button .icon:not(:only-child):first-child, a.button .icon:not(:only-child):first-child, button.button .icon:not(:only-child):first-child {
margin: 0 0.6rem 0 0.2rem;
margin: 0 0.6em 0 0.2em;
}
.button .icon:not(:only-child):last-child, a.button .icon:not(:only-child):last-child, button.button .icon:not(:only-child):last-child {
margin: 0 0.6rem 0 0.2rem;
margin: 0 0.6em 0 0.2em;
}
.button:hover, a.button:hover, button.button:hover {
color: var(--button-hv-fg);

File diff suppressed because one or more lines are too long

View File

@ -24,17 +24,25 @@
next-label="{% translate "Show next" %}"
ref="cover-select"
>
<template #upload-preview="{item}">
<template v-if="item">
<img :src="item.src" class="upload-preview"/>
<template #upload-preview="{upload}">
<img :src="upload.fileURL" class="upload-preview blink"/>
</template>
</template>
<template #default="{item}">
<div class="flex-column">
<div class="flex-grow-1">
<template #default="{item, load, lastUrl}">
<div class="flex-column is-fullheight" v-if="item">
<figure class="flex-grow-1">
<img :src="item.file"/>
</figure>
<div>
<label class="label small">[[ item.name || item.original_filename ]]</label>
<a-action-button
class="has-text-danger small float-right"
icon="fa fa-trash"
confirm="{% translate "Are you sure you want to remove this item from server?" %}"
method="DELETE"
:url="'{% url "api:image-detail" pk="123" %}'.replace('123', item.id)"q
@done="load(lastUrl)">
</a-action-button>
</div>
<label class="label">[[ item.name || item.original_filename ]]</label>
</div>
</template>
</a-select-file>
@ -62,7 +70,7 @@
<input type="datetime-local" name="{{ field.name }}"
value="{{ field.value|date:"Y-m-d" }}T{{ field.value|date:"H:i" }}"/>
{% elif field.name == "content" %}
<textarea name="{{ field.name }}" class="is-fullwidth">{{ field.value|striptags|safe }}</textarea>
<textarea name="{{ field.name }}" class="is-fullwidth height-25">{{ field.value|striptags|safe }}</textarea>
{% else %}
{{ field }}
{% endif %}

View File

@ -1,5 +1,5 @@
<template>
<component :is="tag" @click="call" :class="buttonClass">
<component :is="tag" @click.capture.stop="call" type="button" :class="buttonClass">
<span v-if="promise && runIcon">
<i :class="runIcon"></i>
</span>
@ -27,6 +27,8 @@ export default {
data: Object,
//! Action method, by default, `POST`
method: { type: String, default: 'POST'},
//! If provided open confirmation box before proceeding
confirm: { type: String, default: ''},
//! Action url
url: String,
//! Extra request options
@ -60,6 +62,9 @@ export default {
call() {
if(this.promise || !this.url)
return
if(this.confirm && !confirm(this.confirm))
return
const options = Model.getOptions({
...this.fetchOptions,
method: this.method,

View File

@ -1,27 +1,29 @@
<template>
<div class="a-select-file">
<div :class="['a-select-file-list', listClass]" ref="list">
<div class="flex-column">
<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" v-if="!uploadFile">
<label class="label">{{ uploadLabel }}</label>
<input type="file" @change="previewFile"/>
<input type="file" ref="uploadFile" :name="uploadFieldName" @change="onSubmit"/>
</div>
<slot name="upload-preview" :item="uploadFile"></slot>
<div v-if="uploadFile">
<button class="button secondary" @click="removeUpload">
<span class="icon">
<i class="fa fa-trash"></i>
<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>
<button class="button float-right" @click="doUpload">
<span class="icon">
<i class="fa fa-upload"></i>
</span>
Upload
</button>
</div>
</div>
<!-- tiles -->
<div v-if="prevUrl">
<a href="#" @click="load(prevUrl)">
{{ prevLabel }}
@ -30,7 +32,7 @@
<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"></slot>
<slot :item="item" :load="load" :lastUrl="lastUrl"></slot>
</div>
</template>
@ -55,55 +57,37 @@ export default {
prevLabel: { type: String, default: "Prev" },
nextLabel: { type: String, default: "Next" },
listUrl: { type: String },
uploadUrl: { type: String },
uploadFieldName: { type: String, default: "file" },
uploadLabel: { type: String, default: "Upload a file" },
},
data() {
return {
STATE: {
DEFAULT: 0,
UPLOADING: 1,
},
state: 0,
item: null,
items: [],
uploadFile: null,
uploadUrl: null,
uploadFieldName: null,
uploadCSRF: null,
nextUrl: "",
prevUrl: "",
lastUrl: "",
upload: {},
}
},
methods: {
previewFile(event) {
const [file] = event.target.files
this.uploadFile = file && {
file: file,
src: URL.createObjectURL(file)
}
},
removeUpload() {
this.uploadFile = null;
},
doUpload() {
const formData = new FormData();
formData.append('file', this.uploadFile.file)
formData.append('original_filename', this.uploadFile.file.name)
formData.append('csrfmiddlewaretoken', getCsrf())
fetch(this.uploadUrl, {
method: "POST",
body: formData
}).then(
() => {
this.uploadFile = null;
this.load()
}
)
},
load(url) {
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
@ -116,6 +100,56 @@ export default {
select(item) {
this.item = item;
},
// ---- upload
uploadAbort() {
this.upload.request && this.upload.request.abort()
},
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)
},
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,3 +1,4 @@
import AActionButton from './AActionButton'
import AAutocomplete from './AAutocomplete'
import ACarousel from './ACarousel'
import ADropdown from "./ADropdown"
@ -34,5 +35,5 @@ export const admin = {
export const dashboard = {
...base,
ASelectFile, AModal, APlaylistEditor,
AActionButton, ASelectFile, AModal, APlaylistEditor,
}

View File

@ -151,18 +151,19 @@
.button, a.button, button.button {
font-size: v.$text-size;
display: inline-block;
padding: v.$mp-2;
border: none; //1px var(--button-fg) solid;
padding: v.$mp-2e;
border: none;
justify-content: center;
text-align: center;
// font-size: v.$text-size-medium;
cursor: pointer;
text-decoration: none;
color: var(--button-fg);
background-color: var(--button-bg);
&.square { min-width: 2.5rem; }
&.smaller { font-size: v.$text-size-smaller; }
&.small { font-size: v.$text-size-small; }
&.square { min-width: 2.5em; }
&.secondary { background-color: var(--button-sec-bg); }
.label, label {
@ -172,8 +173,8 @@
.icon {
vertical-align: middle;
&:not(:only-child) {
&:first-child { margin: 0 v.$mp-3 0 v.$mp-1; }
&:last-child { margin: 0 v.$mp-3 0 v.$mp-1; }
&:first-child { margin: 0 v.$mp-3e 0 v.$mp-1e; }
&:last-child { margin: 0 v.$mp-3e 0 v.$mp-1e; }
}
}

View File

@ -1,7 +1,13 @@
@use "./vars" as v;
// ---- text
.text-light { weight: 400; color: var(--text-color-light); }
.bigger { font-size: v.$text-size-bigger !important; }
.big { font-size: v.$text-size-big !important; }
.smaller { font-size: v.$text-size-smaller !important; }
.small { font-size: v.$text-size-small !important; }
// ---- layout
.align-left {
text-align: left;
@ -34,6 +40,20 @@
.ws-nowrap { white-space: nowrap; }
.height-1 { height: 1em; }
.height-2 { height: 2em; }
.height-3 { height: 3em; }
.height-4 { height: 4em; }
.height-5 { height: 5em; }
.height-6 { height: 6em; }
.height-7 { height: 7em; }
.height-8 { height: 8em; }
.height-9 { height: 9em; }
.height-10 { height: 10em; }
.height-15 { height: 15em; }
.height-20 { height: 20em; }
.height-25 { height: 25em; }
// ---- grid
@mixin grid {
display: grid;