chore(front/file-uploader): convert file-uploader view to svelte
This commit is contained in:
parent
79d23a4604
commit
79fb4e28c8
@ -1,11 +0,0 @@
|
|||||||
@import "vars";
|
|
||||||
|
|
||||||
#file-upload {
|
|
||||||
padding: 8px;
|
|
||||||
background: $infoColor;
|
|
||||||
border-radius: 5px;
|
|
||||||
|
|
||||||
.name, .status {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
193
assets/ts/fm.ts
193
assets/ts/fm.ts
@ -1,193 +0,0 @@
|
|||||||
import {applyFormMessages} from "./forms";
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const form = document.querySelector<HTMLFormElement>('#upload-form');
|
|
||||||
if (!form) return;
|
|
||||||
const upload = document.getElementById('file-upload');
|
|
||||||
const uploadLink = document.querySelector<HTMLLinkElement>('#file-upload-link');
|
|
||||||
|
|
||||||
const uploadField = document.querySelector<HTMLInputElement>('#field-upload');
|
|
||||||
|
|
||||||
const neverExpireCheckbox = document.querySelector<HTMLInputElement>('#field-never_expire');
|
|
||||||
const expireAfterDaysField = document.querySelector<HTMLInputElement>('#field-expire_after_days');
|
|
||||||
|
|
||||||
const autogenUrlCheckbox = document.querySelector<HTMLInputElement>('#field-autogen_url');
|
|
||||||
const slugField = document.querySelector<HTMLInputElement>('#field-slug');
|
|
||||||
|
|
||||||
if (expireAfterDaysField && neverExpireCheckbox) {
|
|
||||||
const updateExpireAfterDaysField = () => {
|
|
||||||
expireAfterDaysField.disabled = neverExpireCheckbox.checked;
|
|
||||||
|
|
||||||
const parent = expireAfterDaysField.parentElement?.parentElement;
|
|
||||||
if (parent) {
|
|
||||||
if (neverExpireCheckbox.checked) {
|
|
||||||
parent.classList.add('hidden');
|
|
||||||
} else {
|
|
||||||
parent.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
neverExpireCheckbox.addEventListener('change', updateExpireAfterDaysField);
|
|
||||||
updateExpireAfterDaysField();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slugField && autogenUrlCheckbox) {
|
|
||||||
const updateSlugField = () => {
|
|
||||||
slugField.disabled = autogenUrlCheckbox.checked;
|
|
||||||
|
|
||||||
const parent = slugField.parentElement?.parentElement;
|
|
||||||
if (parent) {
|
|
||||||
if (autogenUrlCheckbox.checked) {
|
|
||||||
parent.classList.add('hidden');
|
|
||||||
} else {
|
|
||||||
parent.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
autogenUrlCheckbox.addEventListener('change', updateSlugField);
|
|
||||||
updateSlugField();
|
|
||||||
}
|
|
||||||
|
|
||||||
let uploadForm: UploadForm | undefined;
|
|
||||||
|
|
||||||
form.addEventListener('submit', e => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (upload && uploadLink && uploadField && uploadField.files && (!uploadForm || uploadForm.isFinished())) {
|
|
||||||
uploadForm = new UploadForm(form, upload, uploadLink, uploadField.files[0].name);
|
|
||||||
uploadForm.updateView();
|
|
||||||
uploadForm.start();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const units = ['K', 'M', 'G', 'T'];
|
|
||||||
|
|
||||||
class UploadForm {
|
|
||||||
private finished: boolean = false;
|
|
||||||
private readonly progressBar: HTMLElement | null;
|
|
||||||
private readonly progressBarContent: HTMLElement | null;
|
|
||||||
private readonly status: HTMLElement | null;
|
|
||||||
private speed: HTMLElement | null;
|
|
||||||
private xferSpeed: number[] = [];
|
|
||||||
private lastTransferTime: number | null = null;
|
|
||||||
private lastLoaded: number = 0;
|
|
||||||
|
|
||||||
private xhr: XMLHttpRequest;
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
private form: HTMLFormElement,
|
|
||||||
private upload: HTMLElement,
|
|
||||||
private uploadLink: HTMLLinkElement,
|
|
||||||
private fileName: string,
|
|
||||||
) {
|
|
||||||
this.progressBar = this.upload.querySelector('.progress-bar');
|
|
||||||
this.progressBarContent = this.progressBar?.querySelector('.content') || null;
|
|
||||||
this.status = this.upload.querySelector('.status');
|
|
||||||
this.speed = this.status?.querySelector('.speed') || null;
|
|
||||||
|
|
||||||
if (!this.progressBar)
|
|
||||||
throw new Error('Invalid html');
|
|
||||||
|
|
||||||
this.xhr = new XMLHttpRequest();
|
|
||||||
this.xhr.responseType = 'json';
|
|
||||||
this.xhr.upload.addEventListener('progress', e => {
|
|
||||||
if (this.progressBar && this.progressBarContent) {
|
|
||||||
if (e.lengthComputable) {
|
|
||||||
this.progressBar.classList.remove('undefined');
|
|
||||||
const percent = (e.loaded / e.total * 100).toFixed(2) + '%';
|
|
||||||
this.progressBar.style.setProperty('--progress', `${percent}`);
|
|
||||||
this.progressBarContent.innerText = percent;
|
|
||||||
this.updateSpeed(e.loaded);
|
|
||||||
} else {
|
|
||||||
this.progressBar.classList.add('undefined');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.xhr.upload.addEventListener('loadstart', () => {
|
|
||||||
this.status?.classList.remove('hidden');
|
|
||||||
});
|
|
||||||
this.xhr.addEventListener('load', () => {
|
|
||||||
this.finished = true;
|
|
||||||
const response = this.xhr.response;
|
|
||||||
console.log('done', response);
|
|
||||||
if (response.status === 'error') {
|
|
||||||
if (response.messages) {
|
|
||||||
this.restoreView();
|
|
||||||
applyFormMessages(this.form, response.messages);
|
|
||||||
}
|
|
||||||
} else if (response.url) {
|
|
||||||
if (this.status) {
|
|
||||||
this.status.innerHTML = 'Done!';
|
|
||||||
}
|
|
||||||
const uploadLinkContent = this.uploadLink.querySelector<HTMLElement>('.content');
|
|
||||||
if (uploadLinkContent) {
|
|
||||||
uploadLinkContent.innerText = response.url;
|
|
||||||
}
|
|
||||||
this.uploadLink.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.xhr.addEventListener('error', (e) => {
|
|
||||||
this.finished = true;
|
|
||||||
console.error('error', e);
|
|
||||||
if (this.status) {
|
|
||||||
this.status.innerHTML = 'Error; upload was interrupted.';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public isFinished(): boolean {
|
|
||||||
return this.finished;
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateView(): void {
|
|
||||||
const uploadName = this.upload.querySelector<HTMLElement>('.name');
|
|
||||||
if (uploadName) {
|
|
||||||
uploadName.innerText = this.fileName;
|
|
||||||
}
|
|
||||||
this.form.classList.add('hidden');
|
|
||||||
this.upload.classList.remove('hidden');
|
|
||||||
if (this.status) {
|
|
||||||
this.status.innerHTML = `Uploading @ <span class="speed">--</span>`;
|
|
||||||
this.speed = this.status.querySelector('.speed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public restoreView(): void {
|
|
||||||
if (this.status) {
|
|
||||||
this.status.classList.add('hidden');
|
|
||||||
}
|
|
||||||
this.upload.classList.add('hidden');
|
|
||||||
this.form.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
public start(): void {
|
|
||||||
const formData = new FormData(this.form);
|
|
||||||
this.xhr.open('POST', this.form.action);
|
|
||||||
this.xhr.send(formData);
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateSpeed(loaded: number): void {
|
|
||||||
const time = new Date().getTime();
|
|
||||||
if (this.lastTransferTime) {
|
|
||||||
this.xferSpeed.push((loaded - this.lastLoaded) / (time - this.lastTransferTime));
|
|
||||||
|
|
||||||
if (this.xferSpeed.length > 100) this.xferSpeed = this.xferSpeed.slice(1);
|
|
||||||
|
|
||||||
let speed = this.xferSpeed.reduce((v, c) => v + c) / this.xferSpeed.length;
|
|
||||||
let unit = 0;
|
|
||||||
while (speed >= 1000 && unit < units.length - 1) {
|
|
||||||
speed /= 1000;
|
|
||||||
unit++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.speed) {
|
|
||||||
this.speed.innerText = speed.toFixed(2) + units[unit] + 'Bps';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.lastTransferTime = time;
|
|
||||||
this.lastLoaded = loaded;
|
|
||||||
}
|
|
||||||
}
|
|
192
src/assets/views/components/FileUpload.svelte
Normal file
192
src/assets/views/components/FileUpload.svelte
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
|
||||||
|
export let file: File;
|
||||||
|
export let form: HTMLFormElement;
|
||||||
|
export let fileFieldName: string = 'file';
|
||||||
|
export let autostart: boolean = false;
|
||||||
|
|
||||||
|
export let inProgress: boolean = false;
|
||||||
|
let xhr: XMLHttpRequest | undefined;
|
||||||
|
let xferSpeed: number[] = [];
|
||||||
|
let lastTransferTime: number | null = null;
|
||||||
|
let lastLoaded: number = 0;
|
||||||
|
let speed: string | undefined;
|
||||||
|
|
||||||
|
export let progress: string = '';
|
||||||
|
export let error: string | undefined = undefined;
|
||||||
|
export let finalUrl: string | undefined = undefined;
|
||||||
|
|
||||||
|
$: file && autoStart();
|
||||||
|
|
||||||
|
const units = ['K', 'M', 'G', 'T'];
|
||||||
|
|
||||||
|
export async function run(): Promise<void> {
|
||||||
|
inProgress = true;
|
||||||
|
try {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
initTrackers();
|
||||||
|
prepareXhrRequest(resolve, reject);
|
||||||
|
if (!xhr) throw new Error('Failed to initialize xhr');
|
||||||
|
sendXhrRequest(xhr);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
onError(e);
|
||||||
|
} finally {
|
||||||
|
inProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnd(file.name, finalUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTrackers() {
|
||||||
|
initSpeedTracker();
|
||||||
|
initProgressTracker();
|
||||||
|
initErrorTracker();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSpeedTracker() {
|
||||||
|
xferSpeed = [];
|
||||||
|
lastTransferTime = null;
|
||||||
|
lastLoaded = 0;
|
||||||
|
speed = '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
function initProgressTracker() {
|
||||||
|
progress = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function initErrorTracker() {
|
||||||
|
error = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareXhrRequest(
|
||||||
|
resolve: (value?: PromiseLike<void>) => void,
|
||||||
|
reject: (err: string | ProgressEvent<XMLHttpRequestEventTarget> | undefined) => void,
|
||||||
|
) {
|
||||||
|
xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.responseType = 'json';
|
||||||
|
xhr.upload.addEventListener('progress', e => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
progress = (e.loaded / e.total * 100).toFixed(2) + '%';
|
||||||
|
updateSpeed(e.loaded);
|
||||||
|
} else {
|
||||||
|
progress = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
xhr.addEventListener('load', () => {
|
||||||
|
const response = xhr.response;
|
||||||
|
if (response) {
|
||||||
|
console.log('done', response);
|
||||||
|
|
||||||
|
if (response.status === 'error') {
|
||||||
|
if (response.messages) {
|
||||||
|
// TODO: display errors // applyFormMessages(this.form, response.messages);
|
||||||
|
return reject(response.messages);
|
||||||
|
}
|
||||||
|
} else if (response.url) {
|
||||||
|
finalUrl = response.url;
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
reject('Malformed response from server.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
xhr.addEventListener('error', (e) => {
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendXhrRequest(xhr: XMLHttpRequest) {
|
||||||
|
const formData = new FormData(form);
|
||||||
|
formData.set(fileFieldName, file);
|
||||||
|
xhr.open('POST', form.action);
|
||||||
|
xhr.send(formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSpeed(loaded: number): void {
|
||||||
|
const time = new Date().getTime();
|
||||||
|
if (lastTransferTime) {
|
||||||
|
xferSpeed.push((loaded - lastLoaded) / (time - lastTransferTime));
|
||||||
|
|
||||||
|
if (xferSpeed.length > 100) xferSpeed = xferSpeed.slice(1);
|
||||||
|
|
||||||
|
let rawSpeed = xferSpeed.reduce((v, c) => v + c) / xferSpeed.length;
|
||||||
|
let unit = 0;
|
||||||
|
while (rawSpeed >= 1000 && unit < units.length - 1) {
|
||||||
|
rawSpeed /= 1000;
|
||||||
|
unit++;
|
||||||
|
}
|
||||||
|
|
||||||
|
speed = rawSpeed.toFixed(2) + units[unit] + 'Bps';
|
||||||
|
}
|
||||||
|
lastTransferTime = time;
|
||||||
|
lastLoaded = loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
autoStart();
|
||||||
|
});
|
||||||
|
|
||||||
|
function autoStart() {
|
||||||
|
if (autostart && !inProgress) {
|
||||||
|
run().catch(onError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export let onEnd = function (name: string, finalUrl: string) {
|
||||||
|
}
|
||||||
|
|
||||||
|
export let onError = function (err) {
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../scss/vars";
|
||||||
|
|
||||||
|
.file-upload {
|
||||||
|
padding: 8px;
|
||||||
|
color: var(--on-subsurface);
|
||||||
|
background: var(--subsurface);
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
.name, .status {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
margin: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
background: #fff1;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: var(--progress);
|
||||||
|
height: 100%;
|
||||||
|
transition: width ease-out 150ms;
|
||||||
|
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="file-upload">
|
||||||
|
<div class="name">{file?.name}</div>
|
||||||
|
<div class="progress-bar" style="--progress: {progress};"><span class="content">{progress}</span></div>
|
||||||
|
<div class="status">Uploading @ {speed}</div>
|
||||||
|
</div>
|
@ -1,104 +0,0 @@
|
|||||||
{% extends 'layouts' %}
|
|
||||||
|
|
||||||
{% set title = app.name + ' - Upload file' %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script src="/js/fm.js"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<h1>Upload files</h1>
|
|
||||||
<p>You are responsible for the files that you upload.</p>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<section class="panel">
|
|
||||||
<h2><i data-feather="upload"></i> Upload a file</h2>
|
|
||||||
|
|
||||||
<form action="{{ route('post-file-frontend') }}" method="POST" enctype="multipart/form-data"
|
|
||||||
id="upload-form">
|
|
||||||
{{ macros.field(_locals, 'file', 'upload', '', 'Choose wisely', 'The maximum upload size is ' + max_upload_size + 'MiB', validation_attributes='required') }}
|
|
||||||
|
|
||||||
{{ macros.field(_locals, 'checkbox', 'never_expire', '', 'Never delete this file') }}
|
|
||||||
{{ macros.field(_locals, 'number', 'expire_after_days', '30', 'How many days to delete this file after', null, validation_attributes='max="1825"') }}
|
|
||||||
|
|
||||||
{{ macros.field(_locals, 'checkbox', 'autogen_url', '', 'Generate url automatically', null, validation_attributes='checked') }}
|
|
||||||
{{ macros.field(_locals, 'text', 'slug', '', 'Custom url slug', 'Example: beautiful_image.jpg sets url to https://'+default_domain+'/beautiful_image.jpg', validation_attributes='disabled') }}
|
|
||||||
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
|
||||||
|
|
||||||
<button type="submit"><i data-feather="upload"></i> Upload</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="file-upload" class="hidden">
|
|
||||||
<div class="name">photo.jpg</div>
|
|
||||||
<div class="progress-bar" style="--progress: 50%;"><span class="content">50%</span></div>
|
|
||||||
<div class="status hidden"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="file-upload-link" class="hidden copyable-text">
|
|
||||||
<div class="title">URL</div>
|
|
||||||
<div class="content"></div>
|
|
||||||
<button class="copy-button"><i data-feather="copy"></i></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% set url = flash('url') %}
|
|
||||||
{% if url | length %}
|
|
||||||
<div class="copyable-text">
|
|
||||||
<div class="title">URL</div>
|
|
||||||
<div class="content">{{ url }}</div>
|
|
||||||
<button class="copy-button"><i data-feather="copy"></i></button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<h2><i data-feather="folder"></i> File list</h2>
|
|
||||||
|
|
||||||
{{ macros.paginate(files.pagination, 'file-uploader', 3) }}
|
|
||||||
|
|
||||||
<table class="data-table file-upload-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th class="table-col-grow">Name</th>
|
|
||||||
<th>Size</th>
|
|
||||||
<th>Expires at</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{% for file in files %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ file.id }}</td>
|
|
||||||
<td class="table-col-grow-cell"><a href="{{ file.getURL() }}"><pre>{{ file.real_name }}</pre></a></td>
|
|
||||||
<td>{{ (file.size / (1024 * 1024)).toFixed(2) }}MB</td>
|
|
||||||
<td>{% if file.expires_at %}{{ file.expires_at.toISOString() }}{% else %}Never{% endif %}</td>
|
|
||||||
<td class="actions">
|
|
||||||
<div>
|
|
||||||
{% if file.shouldBeDeleted() %}
|
|
||||||
Pending deletion
|
|
||||||
{% else %}
|
|
||||||
<button class="copy-button" data-content="{{ file.getURL() }}"><i data-feather="copy"></i> <span class="tip">Copy URL</span></button>
|
|
||||||
|
|
||||||
<form action="{{ route('delete-file-frontend', file.slug) }}" method="post">
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
|
||||||
<button class="button danger" onclick="return confirm('Are you sure you want to delete file {{ file.real_name }}?');"><i data-feather="trash"></i> <span class="tip">Delete</span></button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% else %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="5" class="center">You haven't uploaded any file yet.</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{{ macros.paginate(files.pagination, 'file-uploader', 3) }}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
193
src/assets/views/file-uploader.svelte
Normal file
193
src/assets/views/file-uploader.svelte
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {locals} from "../ts/stores.js";
|
||||||
|
import {route} from "../../common/Routing";
|
||||||
|
import BaseTemplate from "./templates/BaseTemplate.svelte";
|
||||||
|
import Form from "./utils/Form.svelte";
|
||||||
|
import Field from "./utils/Field.svelte";
|
||||||
|
import CopyableText from "./components/CopyableText.svelte";
|
||||||
|
import Pagination from "./components/Pagination.svelte";
|
||||||
|
import Message from "./components/Message.svelte";
|
||||||
|
import Icon from "./utils/Icon.svelte"
|
||||||
|
import FileUpload from "./components/FileUpload.svelte";
|
||||||
|
|
||||||
|
let neverExpire: string;
|
||||||
|
let autogenUrl: string;
|
||||||
|
|
||||||
|
let uploadForm: HTMLFormElement;
|
||||||
|
let selectedFiles: FileList | undefined;
|
||||||
|
let nextFileIndex = 0;
|
||||||
|
let uploading: boolean = false;
|
||||||
|
let currentFile: File | undefined;
|
||||||
|
|
||||||
|
let finishedFileUploads: { name: string, finalUrl: string }[] = [];
|
||||||
|
|
||||||
|
function uploadFiles(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
uploadForm = this;
|
||||||
|
|
||||||
|
processNextUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processNextUpload() {
|
||||||
|
if (nextFileIndex > selectedFiles.length - 1) {
|
||||||
|
nextFileIndex = 0;
|
||||||
|
selectedFiles = undefined;
|
||||||
|
uploading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uploading = true;
|
||||||
|
currentFile = selectedFiles[nextFileIndex++];
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUploadEnd(fileName, finalUrl) {
|
||||||
|
finishedFileUploads = [...finishedFileUploads, {name: fileName, finalUrl: finalUrl}];
|
||||||
|
processNextUpload();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.file-upload-table {
|
||||||
|
@media (max-width: 550px) {
|
||||||
|
> thead > tr > th:nth-child(3),
|
||||||
|
> tbody > tr > td:nth-child(3) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 785px) {
|
||||||
|
> thead > tr > th:nth-child(4),
|
||||||
|
> tbody > tr > td:nth-child(4) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
:global(button .icon) {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(button .tip) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<BaseTemplate title="{$locals.app.name} - Upload file" description="File upload web interface." noH1>
|
||||||
|
<h1>Upload files</h1>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<section class="panel">
|
||||||
|
<h2>
|
||||||
|
<Icon name="upload"/>
|
||||||
|
Upload files
|
||||||
|
</h2>
|
||||||
|
<p>You are responsible for the files that you upload.</p>
|
||||||
|
|
||||||
|
{#each finishedFileUploads as fileUpload}
|
||||||
|
<CopyableText title="{fileUpload.name}" content="{fileUpload.finalUrl}"/>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if uploading}
|
||||||
|
<FileUpload file={currentFile} form={uploadForm} fileFieldName="upload"
|
||||||
|
onEnd={onUploadEnd} autostart/>
|
||||||
|
{:else}
|
||||||
|
<Form action={route('post-file-frontend')} withFiles
|
||||||
|
submitText="Upload" submitIcon="upload"
|
||||||
|
onSubmit={uploadFiles}>
|
||||||
|
<Field type="file" name="upload" placeholder="Choose wisely" icon="file"
|
||||||
|
hint="The maximum upload size is {$locals.max_upload_size} MiB"
|
||||||
|
multiple required
|
||||||
|
bind:fileList={selectedFiles}/>
|
||||||
|
|
||||||
|
<Field type="checkbox" name="never_expire" placeholder="Never delete this file"
|
||||||
|
icon="infinity"
|
||||||
|
bind:value={neverExpire}/>
|
||||||
|
{#if !neverExpire}
|
||||||
|
<Field type="number" name="expire_after_days" icon="clock"
|
||||||
|
placeholder="How many days to delete this file after"
|
||||||
|
initialValue={30} max="1825"/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !selectedFiles || selectedFiles.length <= 1}
|
||||||
|
<Field type="checkbox" name="autogen_url" placeholder="Generate url automatically"
|
||||||
|
icon="zap"
|
||||||
|
bind:value={autogenUrl} initialValue={true}/>
|
||||||
|
{#if !autogenUrl}
|
||||||
|
<Field type="text" name="slug" placeholder="Custom url slug" icon="link"
|
||||||
|
hint="Example: beautiful_image.jpg sets url to https://{$locals.default_domain}/beautiful_image.jpg"/>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<Message type="info" discreet sticky
|
||||||
|
content="When upload multiple files, URLs will be generated automatically."/>
|
||||||
|
{/if}
|
||||||
|
</Form>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $locals.flash.url}
|
||||||
|
<CopyableText title="URL" content={$locals.flash.url}/>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>
|
||||||
|
<Icon name="folder"/>
|
||||||
|
File list
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Pagination pagination={$locals.pagination} routeName="file-uploader" contextSize={3}/>
|
||||||
|
|
||||||
|
<div class="data-table-container">
|
||||||
|
<table class="data-table file-upload-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th class="table-col-grow">Name</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Expires at</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{#each $locals.files as file}
|
||||||
|
<tr>
|
||||||
|
<td>{file.id}</td>
|
||||||
|
<td class="table-col-grow-cell"><a href="{file.url}">
|
||||||
|
<pre>{file.real_name}</pre>
|
||||||
|
</a></td>
|
||||||
|
<td>{(file.size / (1024 * 1024)).toFixed(2)}MB</td>
|
||||||
|
<td>
|
||||||
|
{#if file.expires_at}{file.expires_at}{:else}Never{/if}
|
||||||
|
</td><!-- todo: convert that to relative, human readable time -->
|
||||||
|
<td class="actions">
|
||||||
|
{#if file.shouldBeDeleted}
|
||||||
|
Pending deletion
|
||||||
|
{:else}
|
||||||
|
<button class="copy-button" data-content="{file.url}">
|
||||||
|
<Icon name="copy"/>
|
||||||
|
<span class="tip">Copy URL</span></button><!-- todo -->
|
||||||
|
|
||||||
|
<Form action={route('delete-file-frontend', file.slug)} button
|
||||||
|
submitIcon="trash" submitText="delete" submitClass="danger"
|
||||||
|
confirm="Are you sure you want to delete file {file.real_name}?"/>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{:else}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="center">You haven't uploaded any file yet.</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Pagination pagination={$locals.pagination} routeName="file-uploader" contextSize={3}/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</BaseTemplate>
|
@ -27,11 +27,18 @@ export default class FileController extends Controller {
|
|||||||
protected async getFileUploader(req: Request, res: Response): Promise<void> {
|
protected async getFileUploader(req: Request, res: Response): Promise<void> {
|
||||||
const allowedDomains = config.get<string[]>('allowed_url_domains');
|
const allowedDomains = config.get<string[]>('allowed_url_domains');
|
||||||
const user = req.as(RequireAuthMiddleware).getUser();
|
const user = req.as(RequireAuthMiddleware).getUser();
|
||||||
|
const files = await FileModel.paginateForUser(req, 25, user.getOrFail('id'));
|
||||||
res.render('file-uploader', {
|
res.render('file-uploader', {
|
||||||
max_upload_size: config.get<string>('max_upload_size'),
|
max_upload_size: config.get<string>('max_upload_size'),
|
||||||
allowed_domains: allowedDomains,
|
allowed_domains: allowedDomains,
|
||||||
default_domain: allowedDomains[config.get<number>('default_url_domain_for_files')],
|
default_domain: allowedDomains[config.get<number>('default_url_domain_for_files')],
|
||||||
files: await FileModel.paginateForUser(req, 25, user.getOrFail('id')),
|
files: files.map(file => ({
|
||||||
|
...file,
|
||||||
|
shouldBeDeleted: file.shouldBeDeleted(),
|
||||||
|
url: file.getURL(),
|
||||||
|
expires_at: file.expires_at?.toISOString(),
|
||||||
|
})),
|
||||||
|
pagination: files.pagination?.serialize(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import {Request} from "express";
|
|||||||
import User from "swaf/auth/models/User";
|
import User from "swaf/auth/models/User";
|
||||||
import {route} from "swaf/common/Routing";
|
import {route} from "swaf/common/Routing";
|
||||||
import Model from "swaf/db/Model";
|
import Model from "swaf/db/Model";
|
||||||
|
import {ModelQueryResult} from "swaf/db/ModelQuery";
|
||||||
|
|
||||||
import URLRedirect from "./URLRedirect.js";
|
import URLRedirect from "./URLRedirect.js";
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ export default class FileModel extends Model {
|
|||||||
return await this.select().where('slug', slug).first();
|
return await this.select().where('slug', slug).first();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async paginateForUser(req: Request, perPage: number, user_id: number): Promise<FileModel[]> {
|
public static async paginateForUser(req: Request, perPage: number, user_id: number): Promise<ModelQueryResult<FileModel>> {
|
||||||
req.params.sortBy = 'created_at';
|
req.params.sortBy = 'created_at';
|
||||||
req.params.sortDirection = 'DESC';
|
req.params.sortDirection = 'DESC';
|
||||||
return await this.paginate(req, perPage, this.select().where('user_id', user_id));
|
return await this.paginate(req, perPage, this.select().where('user_id', user_id));
|
||||||
|
Loading…
Reference in New Issue
Block a user