mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-11-01 10:56:31 +08:00
feature (upload): upload queue that show progress with abort and retry - #267
This commit is contained in:
1
client/assets/img/refresh.svg
Normal file
1
client/assets/img/refresh.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
||||
|
After Width: | Height: | Size: 340 B |
1
client/assets/img/stop.svg
Normal file
1
client/assets/img/stop.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M6 6h12v12H6z"/></svg>
|
||||
|
After Width: | Height: | Size: 151 B |
@ -39,6 +39,8 @@ import img_info from '../assets/img/info.svg';
|
||||
import img_fullscreen from '../assets/img/fullscreen.svg';
|
||||
import img_camera from '../assets/img/camera.svg';
|
||||
import img_location from '../assets/img/location.svg';
|
||||
import img_stop from '../assets/img/stop.svg';
|
||||
import img_refresh from '../assets/img/refresh.svg';
|
||||
export const img_placeholder = "/assets/icons/placeholder.png";
|
||||
|
||||
export const Icon = (props) => {
|
||||
@ -126,6 +128,10 @@ export const Icon = (props) => {
|
||||
img = img_camera;
|
||||
}else if(props.name === 'location'){
|
||||
img = img_location;
|
||||
} else if (props.name === 'stop') {
|
||||
img = img_stop;
|
||||
} else if (props.name === 'refresh') {
|
||||
img = img_refresh;
|
||||
}else{
|
||||
throw('unknown icon: "'+props.name+"'");
|
||||
}
|
||||
|
||||
@ -21,3 +21,4 @@ export { Dropdown, DropdownButton, DropdownList, DropdownItem } from './dropdown
|
||||
export { MapShot } from './mapshot';
|
||||
export { LoggedInOnly, ErrorPage, LoadingPage } from './decorator';
|
||||
export { FormBuilder } from './formbuilder';
|
||||
export { UploadQueue } from './upload_queue';
|
||||
|
||||
381
client/components/upload_queue.js
Normal file
381
client/components/upload_queue.js
Normal file
@ -0,0 +1,381 @@
|
||||
import React from 'react';
|
||||
import Path from 'path';
|
||||
|
||||
import { Files } from '../model/';
|
||||
import { confirm, notify, upload } from '../helpers/';
|
||||
import { Icon, NgIf } from './';
|
||||
import './upload_queue.scss';
|
||||
|
||||
const MAX_POOL_SIZE = 15;
|
||||
|
||||
function humanFileSize(bytes, si) {
|
||||
var thresh = si ? 1000 : 1024;
|
||||
if (Math.abs(bytes) < thresh) {
|
||||
return bytes.toFixed(1) + ' B';
|
||||
}
|
||||
var units = si
|
||||
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
var u = -1;
|
||||
do {
|
||||
bytes /= thresh;
|
||||
++u;
|
||||
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
|
||||
return bytes.toFixed(1) + ' ' + units[u];
|
||||
}
|
||||
|
||||
export class UploadQueue extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
timeout: 1,
|
||||
running: false,
|
||||
files: [],
|
||||
processes: [],
|
||||
currents: [],
|
||||
failed: [],
|
||||
finished: [],
|
||||
prior_status: {},
|
||||
progress: {},
|
||||
speed: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (typeof this.state.timeout === "number") {
|
||||
this.setState({
|
||||
timeout: window.setTimeout(() => {
|
||||
this.componentDidMount();
|
||||
}, Math.random() * 1000 + 200)
|
||||
});
|
||||
}
|
||||
upload.subscribe((path, files) => this.addFiles(path, files));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.clearTimeout(this.state.timeout);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setState({
|
||||
files: [],
|
||||
processes: [],
|
||||
currents: [],
|
||||
failed: [],
|
||||
finished: [],
|
||||
prior_status: {},
|
||||
progress: {},
|
||||
speed: [],
|
||||
});
|
||||
}
|
||||
|
||||
emphasis(path) {
|
||||
notify.send(path.split("/").join(" / "), "info");
|
||||
}
|
||||
|
||||
runner(id) {
|
||||
let current_process = null;
|
||||
let processes = [...this.state.processes];
|
||||
if (processes.length === 0 || !this.state.running) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var i;
|
||||
for (i = 0; i < processes.length; i++) {
|
||||
if (
|
||||
// init: getting started with creation of files/folders
|
||||
processes[i].parent === null ||
|
||||
// running: make sure we've created the parent folder
|
||||
this.state.prior_status[processes[i].parent] === true
|
||||
) {
|
||||
current_process = this.state.processes[i];
|
||||
processes.splice(i, 1);
|
||||
this.setState({
|
||||
processes,
|
||||
currents: [...this.state.currents, current_process],
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (current_process) {
|
||||
return current_process.fn(id)
|
||||
.then(() => {
|
||||
if (current_process.id) {
|
||||
this.setState({
|
||||
prior_status: {
|
||||
...this.state.prior_status,
|
||||
[current_process.id]: true
|
||||
}
|
||||
})
|
||||
}
|
||||
this.setState({
|
||||
currents: this.state.currents.filter((c) => c.path != current_process.path),
|
||||
finished: [...this.state.finished, current_process],
|
||||
})
|
||||
return this.runner(id);
|
||||
})
|
||||
.catch((err) => {
|
||||
current_process.err = err;
|
||||
this.setState({
|
||||
failed: [...this.state.failed, current_process],
|
||||
currents: this.state.currents.filter((c) => c.path != current_process.path),
|
||||
});
|
||||
let { message } = err;
|
||||
if (message !== 'aborted') {
|
||||
notify.send(err, 'error');
|
||||
}
|
||||
return this.runner(id);
|
||||
});
|
||||
} else {
|
||||
function waitABit() {
|
||||
return new Promise((done) => {
|
||||
window.setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
done();
|
||||
});
|
||||
}, 250);
|
||||
});
|
||||
}
|
||||
return waitABit().then(() => this.runner(id));
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress(path, e) {
|
||||
if (e.lengthComputable) {
|
||||
let prev = this.state.progress[path];
|
||||
this.setState({
|
||||
progress: {
|
||||
...this.state.progress,
|
||||
[path]: {
|
||||
...prev,
|
||||
percent: Math.round(100 * e.loaded / e.total),
|
||||
loaded: e.loaded,
|
||||
time: Date.now(),
|
||||
prev: prev ? prev : null,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateAbort(path, abort) {
|
||||
this.setState({
|
||||
progress: {
|
||||
...this.state.progress,
|
||||
[path]: {
|
||||
...this.state.progress[path],
|
||||
abort,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
addFiles(path, files) {
|
||||
const processes = files.map((file) => {
|
||||
let original_path = file.path;
|
||||
file.path = Path.join(path, file.path);
|
||||
if (file.type === 'file') {
|
||||
if (files.length < 150) Files.touch(file.path, file.file, 'prepare_only');
|
||||
return {
|
||||
path: original_path,
|
||||
parent: file._prior || null,
|
||||
fn: Files.touch.bind(
|
||||
Files, file.path, file.file, 'execute_only',
|
||||
{
|
||||
progress: (e) => this.updateProgress(original_path, e),
|
||||
abort: (x) => this.updateAbort(original_path, x),
|
||||
}
|
||||
)
|
||||
};
|
||||
} else {
|
||||
Files.mkdir(file.path, 'prepare_only');
|
||||
return {
|
||||
id: file._id || null,
|
||||
path: original_path,
|
||||
parent: file._prior || null,
|
||||
fn: Files.mkdir.bind(Files, file.path, 'execute_only')
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
processes: [...this.state.processes, ...processes],
|
||||
files: [...this.state.files, ...files],
|
||||
});
|
||||
this.start();
|
||||
}
|
||||
|
||||
retryFiles(process) {
|
||||
this.setState({
|
||||
processes: [...this.state.processes, process],
|
||||
failed: this.state.failed.filter((c) => c.path != process.path),
|
||||
});
|
||||
window.setTimeout(() => this.start(), 300);
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.state.running) {
|
||||
window.setTimeout(() => this.calcSpeed(), 500);
|
||||
this.setState({
|
||||
running: true,
|
||||
});
|
||||
|
||||
Promise.all(Array.apply(null, Array(MAX_POOL_SIZE)).map((process, index) => {
|
||||
return this.runner();
|
||||
})).then(() => {
|
||||
window.setTimeout(() => {
|
||||
notify.send('Upload completed', 'success');
|
||||
}, 300);
|
||||
this.setState({ running: false });
|
||||
}).catch((err) => {
|
||||
notify.send(err, 'error');
|
||||
this.setState({ running: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
abort(p) {
|
||||
let info = this.state.progress[p.path];
|
||||
if (info && info.abort) {
|
||||
info.abort();
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentPercent(path) {
|
||||
let info = this.state.progress[path];
|
||||
if (info && info.percent) {
|
||||
return this.state.progress[path].percent + "%";
|
||||
}
|
||||
return "0%"
|
||||
}
|
||||
|
||||
calcSpeed() {
|
||||
let now = Date.now();
|
||||
let curSpeed = [];
|
||||
for (const [key, value] of Object.entries(this.state.progress)) {
|
||||
if (value.prev && now - value.time < 5 * 1000) {
|
||||
let bytes = value.loaded - value.prev.loaded;
|
||||
let timeMs = value.time - value.prev.time;
|
||||
curSpeed.push(1000 * bytes / timeMs);
|
||||
}
|
||||
}
|
||||
let avgSpeed = curSpeed.reduce(function (p, c, i) { return p + (c - p) / (i + 1) }, 0);
|
||||
this.setState({
|
||||
speed: [...this.state.speed, avgSpeed].slice(-5),
|
||||
});
|
||||
if (this.state.running) {
|
||||
window.setTimeout(() => this.calcSpeed(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
getState() {
|
||||
let avgSpeed = this.state.speed.reduce(function (p, c, i) { return p + (c - p) / (i + 1) }, 0);
|
||||
let speedStr = ""
|
||||
if (avgSpeed > 0) {
|
||||
speedStr = " ~ " + humanFileSize(avgSpeed) + "/s";
|
||||
}
|
||||
if (this.state.running) {
|
||||
return "Running..." + speedStr
|
||||
}
|
||||
return "Done" + speedStr
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if(this.state.running) {
|
||||
confirm.now(
|
||||
"Abort current uploads?",
|
||||
() => {
|
||||
this.setState({
|
||||
running: false,
|
||||
});
|
||||
this.state.currents.map(p => this.abort(p));
|
||||
window.setTimeout(() => this.reset(), 30);
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
} else {
|
||||
this.reset()
|
||||
}
|
||||
}
|
||||
|
||||
renderRows(arr, state, col_state, action) {
|
||||
let row_class = state + "_color";
|
||||
return arr.slice(0, 1000).map((process, i) => {
|
||||
return (
|
||||
<div className={"file_row " + row_class} key={i}>
|
||||
<div
|
||||
className="file_path"
|
||||
onClick={() => this.emphasis(process.path)}
|
||||
>
|
||||
{process.path.replace(/\//, '')}
|
||||
</div>
|
||||
{col_state(process)}
|
||||
<div className="file_control">
|
||||
{action ? action(process): (<span></span>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let { finished, files, processes, currents, failed } = this.state;
|
||||
let totalFiles = files.length;
|
||||
return (
|
||||
<NgIf cond={totalFiles > 0}>
|
||||
<div className="component_stats">
|
||||
<h2>
|
||||
CURRENT UPLOAD
|
||||
<div className="count_block">
|
||||
<span className="completed">{finished.length}</span>
|
||||
<span className="grandTotal">{totalFiles}</span>
|
||||
</div>
|
||||
<Icon name="close" onClick={(e) => this.onClose()} />
|
||||
</h2>
|
||||
<h3>{this.getState()}</h3>
|
||||
<div className="stats_content">
|
||||
{this.renderRows(
|
||||
finished,
|
||||
"done",
|
||||
(_) => (<div className="file_state file_state_done">Done</div>),
|
||||
)}
|
||||
{this.renderRows(
|
||||
currents,
|
||||
"current",
|
||||
(p) => (
|
||||
<div className="file_state file_state_current">
|
||||
{this.getCurrentPercent(p.path)}
|
||||
</div>
|
||||
),
|
||||
(p) => (
|
||||
<Icon name="stop" onClick={(e) => this.abort(p)} ></Icon>
|
||||
)
|
||||
)}
|
||||
{this.renderRows(
|
||||
processes,
|
||||
"todo",
|
||||
(_) => (
|
||||
<div className="file_state file_state_todo">Waiting</div>
|
||||
)
|
||||
)}
|
||||
{this.renderRows(
|
||||
failed,
|
||||
"error",
|
||||
(p) => (
|
||||
(p.err && p.err.message == 'aborted')
|
||||
?
|
||||
<div className="file_state file_state_error">Aborted</div>
|
||||
:
|
||||
<div className="file_state file_state_error">Error</div>
|
||||
),
|
||||
(p) => (
|
||||
<Icon name="refresh" onClick={(e) => this.retryFiles(p)} ></Icon>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</NgIf>
|
||||
);
|
||||
}
|
||||
}
|
||||
99
client/components/upload_queue.scss
Normal file
99
client/components/upload_queue.scss
Normal file
@ -0,0 +1,99 @@
|
||||
.component_stats{
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 999;
|
||||
max-width: 300px;
|
||||
|
||||
box-shadow: 1px 2px 20px rgba(0, 0, 0, 0.1);
|
||||
background: white;
|
||||
padding: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 1.2em;
|
||||
font-weight: 100;
|
||||
.percent{color: var(--emphasis-primary);}
|
||||
.count_block {
|
||||
display: inline;
|
||||
margin-left: 10px;
|
||||
span.grandTotal{
|
||||
font-size: 0.8em;
|
||||
color: var(--emphasis-secondary);
|
||||
&:before { content: "/"; }
|
||||
}
|
||||
span.completed{
|
||||
color: var(--emphasis-secondary);
|
||||
}
|
||||
}
|
||||
.component_icon {
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
width: 32px;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 1.0em;
|
||||
font-weight: 100;
|
||||
}
|
||||
.stats_content {
|
||||
clear: both;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
font-size: 0.85em;
|
||||
|
||||
.error_color{
|
||||
color: var(--error);
|
||||
}
|
||||
.todo_color{
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
.file_row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
.file_path {
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-basis: 100%;
|
||||
flex: 1;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.file_state {
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.file_control img {
|
||||
display: block !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.file_control {
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
img {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -33,7 +33,7 @@ export function http_get(url, type = 'json'){
|
||||
});
|
||||
}
|
||||
|
||||
export function http_post(url, data, type = 'json'){
|
||||
export function http_post(url, data, type = 'json', params){
|
||||
return new Promise((done, err) => {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", url, true);
|
||||
@ -43,6 +43,9 @@ export function http_post(url, data, type = 'json'){
|
||||
data = JSON.stringify(data);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
}
|
||||
if (params !== undefined && params.progress) {
|
||||
xhr.upload.addEventListener("progress", params.progress, false);
|
||||
}
|
||||
xhr.send(data);
|
||||
xhr.onload = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
@ -65,6 +68,12 @@ export function http_post(url, data, type = 'json'){
|
||||
xhr.onerror = function(){
|
||||
handle_error_response(xhr, err)
|
||||
}
|
||||
if (params !== undefined && params.abort) {
|
||||
params.abort(() => {
|
||||
xhr.abort();
|
||||
err({ message: 'aborted' });
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -14,3 +14,4 @@ export { leftPad, format, copyToClipboard } from './common';
|
||||
export { getMimeType } from './mimetype';
|
||||
export { settings_get, settings_put } from './settings';
|
||||
export { FormObjToJSON, createFormBackend, autocomplete } from './form';
|
||||
export { upload } from './upload';
|
||||
|
||||
16
client/helpers/upload.js
Normal file
16
client/helpers/upload.js
Normal file
@ -0,0 +1,16 @@
|
||||
const Upload = function () {
|
||||
let fn = null;
|
||||
|
||||
return {
|
||||
add: function (path, files) {
|
||||
if (!fn) { return window.setTimeout(() => this.add(path, files), 50); }
|
||||
fn(path, files);
|
||||
return Promise.resolve();
|
||||
},
|
||||
subscribe: function (_fn) {
|
||||
fn = _fn;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const upload = new Upload();
|
||||
@ -261,7 +261,7 @@ class FileSystem{
|
||||
}
|
||||
}
|
||||
|
||||
touch(path, file, step){
|
||||
touch(path, file, step, params){
|
||||
const origin_path = pathBuilder(this.current_path, basename(path), 'file'),
|
||||
destination_path = path;
|
||||
|
||||
@ -308,7 +308,7 @@ class FileSystem{
|
||||
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
|
||||
let formData = new window.FormData();
|
||||
formData.append('file', file);
|
||||
return http_post(url, formData, 'multipart');
|
||||
return http_post(url, formData, 'multipart', params);
|
||||
}else{
|
||||
const url = appendShareToUrl('/api/files/touch?path='+prepare(path));
|
||||
return http_get(url);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Files } from '../model/';
|
||||
import { notify, alert, currentShare } from '../helpers/';
|
||||
import { notify, upload } from '../helpers/';
|
||||
import Path from 'path';
|
||||
import { Observable } from "rxjs/Observable";
|
||||
|
||||
@ -120,9 +120,6 @@ export const onMultiRename = function(arrOfPath){
|
||||
* 3. user is coming from a upload form button as he doesn't have drag and drop with files
|
||||
*/
|
||||
export const onUpload = function(path, e){
|
||||
const MAX_POOL_SIZE = 15;
|
||||
let PRIOR_STATUS = {};
|
||||
|
||||
let extractFiles = null;
|
||||
if(e.dataTransfer === undefined){ // case 3
|
||||
extractFiles = extract_upload_crappy_hack_but_official_way(e.target);
|
||||
@ -141,165 +138,7 @@ export const onUpload = function(path, e){
|
||||
})
|
||||
}
|
||||
|
||||
extractFiles.then((files) => {
|
||||
var failed = [],
|
||||
currents = [];
|
||||
|
||||
const processes = files.map((file) => {
|
||||
let original_path = file.path;
|
||||
file.path = Path.join(path, file.path);
|
||||
if(file.type === 'file'){
|
||||
if(files.length < 150) Files.touch(file.path, file.file, 'prepare_only');
|
||||
return {
|
||||
path: original_path,
|
||||
parent: file._prior || null,
|
||||
fn: Files.touch.bind(Files, file.path, file.file, 'execute_only')
|
||||
};
|
||||
}else{
|
||||
Files.mkdir(file.path, 'prepare_only');
|
||||
return {
|
||||
id: file._id || null,
|
||||
path: original_path,
|
||||
parent: file._prior || null,
|
||||
fn: Files.mkdir.bind(Files, file.path, 'execute_only')
|
||||
};
|
||||
}
|
||||
});
|
||||
class Stats extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {timeout: 1};
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
if(typeof this.state.timeout === "number"){
|
||||
this.setState({
|
||||
timeout: window.setTimeout(() => {
|
||||
this.componentDidMount();
|
||||
}, Math.random()*1000+200)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
window.clearTimeout(this.state.timeout);
|
||||
}
|
||||
|
||||
emphasis(path){
|
||||
notify.send(path.split("/").join(" / "), "info");
|
||||
}
|
||||
|
||||
render() {
|
||||
const percent = Math.floor(100 * (files.length - processes.length - currents.length) / files.length);
|
||||
return (
|
||||
<div className="component_stats">
|
||||
<h2>
|
||||
UPLOADING <span className="percent">({percent}%)</span>
|
||||
<div>
|
||||
<span className="completed">{files.length - processes.length - currents.length}</span>
|
||||
<span className="grandTotal">{files.length}</span>
|
||||
</div>
|
||||
</h2>
|
||||
<div className="stats_content">
|
||||
{
|
||||
currents.slice(0, 1000).map((process, i) => {
|
||||
return (
|
||||
<div onClick={() => this.emphasis(process.path)} className="current_color" key={i}>{process.path.replace(/\//, '')}</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
{
|
||||
processes.slice(0, 1000).map((process, i) => {
|
||||
return (
|
||||
<div onClick={() => this.emphasis(process.path)} className="todo_color" key={i}>{process.path.replace(/\//, '')}</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
{
|
||||
failed.slice(0, 500).map((process, i) => {
|
||||
return (
|
||||
<div onClick={() => this.emphasis(process.path)} className="error_color" key={i}>{process.path}</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function runner(id){
|
||||
let current_process = null;
|
||||
if(processes.length === 0) return Promise.resolve();
|
||||
|
||||
var i;
|
||||
for(i=0; i<processes.length; i++){
|
||||
if(
|
||||
// init: getting started with creation of files/folders
|
||||
processes[i].parent === null ||
|
||||
// running: make sure we've created the parent folder
|
||||
PRIOR_STATUS[processes[i].parent] === true
|
||||
){
|
||||
current_process = processes[i];
|
||||
processes.splice(i, 1);
|
||||
currents.push(current_process);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(current_process){
|
||||
return current_process.fn(id)
|
||||
.then(() => {
|
||||
if(current_process.id) PRIOR_STATUS[current_process.id] = true;
|
||||
currents = currents.filter((c) => c.path != current_process.path);
|
||||
return runner(id);
|
||||
})
|
||||
.catch((err) => {
|
||||
failed.push(current_process);
|
||||
currents = currents.filter((c) => c.path != current_process.path);
|
||||
notify.send(err, 'error');
|
||||
return runner(id);
|
||||
});
|
||||
}else{
|
||||
return waitABit()
|
||||
.then(() => runner(id));
|
||||
|
||||
function waitABit(){
|
||||
return new Promise((done) => {
|
||||
window.setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
done();
|
||||
});
|
||||
}, 250);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(files.length >= 5){
|
||||
alert.now(<Stats/>, () => {});
|
||||
}
|
||||
Promise.all(Array.apply(null, Array(MAX_POOL_SIZE)).map((process,index) => {
|
||||
return runner();
|
||||
})).then(() => {
|
||||
// remove the popup
|
||||
if(failed.length === 0){
|
||||
var e = new Event("keydown");
|
||||
e.keyCode = 27;
|
||||
window.dispatchEvent(e);
|
||||
}
|
||||
currents = [];
|
||||
// display message
|
||||
window.setTimeout(() => {
|
||||
notify.send('Upload completed', 'success');
|
||||
}, 300);
|
||||
}).catch((err) => {
|
||||
currents = [];
|
||||
notify.send(err, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
extractFiles.then((files) => upload.add(path, files));
|
||||
|
||||
// adapted from: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
|
||||
function _rand_id(){
|
||||
|
||||
@ -42,43 +42,3 @@
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.component_stats{
|
||||
h2{
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 1.2em;
|
||||
font-weight: 100;
|
||||
.percent{color: var(--emphasis-primary);}
|
||||
> div{
|
||||
float: right;
|
||||
span.grandTotal{
|
||||
font-size: 0.8em;
|
||||
color: var(--emphasis-secondary);
|
||||
&:before { content: "/"; }
|
||||
}
|
||||
span.completed{
|
||||
color: var(--emphasis-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.stats_content {
|
||||
clear: both;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
font-size: 0.85em;
|
||||
div{
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: calc(100% - 10px);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.error_color{
|
||||
color: var(--error);
|
||||
}
|
||||
.todo_color{
|
||||
color: var(--light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { BrowserRouter, Route, IndexRoute, Switch } from 'react-router-dom';
|
||||
import { NotFoundPage, ConnectPage, HomePage, SharePage, LogoutPage, FilesPage, ViewerPage } from './pages/';
|
||||
import { URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT } from './helpers/';
|
||||
import { Bundle, ModalPrompt, ModalAlert, ModalConfirm, Notification, Audio, Video } from './components/';
|
||||
import { Bundle, ModalPrompt, ModalAlert, ModalConfirm, Notification, Audio, Video, UploadQueue } from './components/';
|
||||
|
||||
const AdminPage = (props) => (
|
||||
<Bundle loader={import(/* webpackChunkName: "admin" */"./pages/adminpage")} symbol="AdminPage">
|
||||
@ -27,7 +27,7 @@ export default class AppRouter extends React.Component {
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
<ModalPrompt /> <ModalAlert /> <ModalConfirm />
|
||||
<Notification />
|
||||
<Notification /> <UploadQueue/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user