kopia lustrzana https://github.com/OpenDroneMap/WebODM
Refactored ProjectListItem, client-side file resize feature
rodzic
7815514ee9
commit
8703af0dae
|
@ -2,17 +2,20 @@ import '../css/NewTaskPanel.scss';
|
|||
import React from 'react';
|
||||
import EditTaskForm from './EditTaskForm';
|
||||
import PropTypes from 'prop-types';
|
||||
import Storage from '../classes/Storage';
|
||||
|
||||
class NewTaskPanel extends React.Component {
|
||||
static defaultProps = {
|
||||
uploading: false,
|
||||
name: ""
|
||||
name: "",
|
||||
filesCount: 0,
|
||||
showResize: false
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
onSave: PropTypes.func.isRequired,
|
||||
name: PropTypes.string,
|
||||
uploading: PropTypes.bool
|
||||
filesCount: PropTypes.number,
|
||||
showResize: PropTypes.bool
|
||||
};
|
||||
|
||||
constructor(props){
|
||||
|
@ -20,30 +23,43 @@ class NewTaskPanel extends React.Component {
|
|||
|
||||
this.state = {
|
||||
name: props.name,
|
||||
editing: true,
|
||||
editTaskFormLoaded: false
|
||||
editTaskFormLoaded: false,
|
||||
resize: Storage.getItem('do_resize') !== null ? Storage.getItem('do_resize') == "1" : true,
|
||||
resizeSize: parseInt(Storage.getItem('resize_size')) || 2048
|
||||
};
|
||||
|
||||
this.save = this.save.bind(this);
|
||||
this.edit = this.edit.bind(this);
|
||||
this.handleFormTaskLoaded = this.handleFormTaskLoaded.bind(this);
|
||||
this.getTaskInfo = this.getTaskInfo.bind(this);
|
||||
this.setResize = this.setResize.bind(this);
|
||||
this.handleResizeSizeChange = this.handleResizeSizeChange.bind(this);
|
||||
}
|
||||
|
||||
save(e){
|
||||
e.preventDefault();
|
||||
this.setState({editing: false});
|
||||
this.taskForm.saveLastPresetToStorage();
|
||||
Storage.setItem('resize_size', this.state.resizeSize);
|
||||
Storage.setItem('do_resize', this.state.resize ? "1" : "0");
|
||||
if (this.props.onSave) this.props.onSave(this.getTaskInfo());
|
||||
}
|
||||
|
||||
getTaskInfo(){
|
||||
return this.taskForm.getTaskInfo();
|
||||
return Object.assign(this.taskForm.getTaskInfo(), {
|
||||
resizeTo: (this.state.resize && this.state.resizeSize > 0) ? this.state.resizeSize : null
|
||||
});
|
||||
}
|
||||
|
||||
edit(e){
|
||||
e.preventDefault();
|
||||
this.setState({editing: true});
|
||||
setResize(flag){
|
||||
return e => {
|
||||
this.setState({resize: flag});
|
||||
}
|
||||
}
|
||||
|
||||
handleResizeSizeChange(e){
|
||||
// Remove all non-digit characters
|
||||
let n = parseInt(e.target.value.replace(/[^\d]*/g, ""));
|
||||
if (isNaN(n)) n = "";
|
||||
this.setState({resizeSize: n});
|
||||
}
|
||||
|
||||
handleFormTaskLoaded(){
|
||||
|
@ -51,36 +67,59 @@ class NewTaskPanel extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
if (this.props.uploading || this.state.editing){
|
||||
// Done editing, but still uploading
|
||||
return (
|
||||
<div className="new-task-panel theme-background-highlight">
|
||||
<div className={"form-horizontal " + (this.state.editing ? "" : "hide")}>
|
||||
<p>{this.props.uploading ?
|
||||
"Your images are being uploaded. In the meanwhile, check these additional options:"
|
||||
: "Please check these additional options:"}</p>
|
||||
<EditTaskForm
|
||||
onFormLoaded={this.handleFormTaskLoaded}
|
||||
ref={(domNode) => { if (domNode) this.taskForm = domNode; }}
|
||||
/>
|
||||
{this.state.editTaskFormLoaded ?
|
||||
<div className="form-group">
|
||||
<div className="col-sm-offset-2 col-sm-10 text-right">
|
||||
<button type="submit" className="btn btn-primary" onClick={this.save}><i className="glyphicon glyphicon-saved"></i> {this.props.uploading ? "Save" : "Start Processing"}</button>
|
||||
</div>
|
||||
return (
|
||||
<div className="new-task-panel theme-background-highlight">
|
||||
<div className="form-horizontal">
|
||||
<p>{this.props.filesCount} files selected. Please check these additional options:</p>
|
||||
<EditTaskForm
|
||||
onFormLoaded={this.handleFormTaskLoaded}
|
||||
ref={(domNode) => { if (domNode) this.taskForm = domNode; }}
|
||||
/>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="col-sm-2 control-label">Resize Images</label>
|
||||
<div className="col-sm-10">
|
||||
<div className="btn-group">
|
||||
<button type="button" className="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
{this.state.resize ?
|
||||
"Yes" : "Skip"} <span className="caret"></span>
|
||||
</button>
|
||||
<ul className="dropdown-menu">
|
||||
<li>
|
||||
<a href="javascript:void(0);"
|
||||
onClick={this.setResize(true)}>
|
||||
<i style={{opacity: this.state.resize ? 1 : 0}} className="fa fa-check"></i> Yes</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:void(0);"
|
||||
onClick={this.setResize(false)}>
|
||||
<i style={{opacity: !this.state.resize ? 1 : 0}} className="fa fa-check"></i> Skip</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
: ""}
|
||||
<div className={"resize-control " + (!this.state.resize ? "hide" : "")}>
|
||||
<input
|
||||
type="number"
|
||||
step="100"
|
||||
className="form-control"
|
||||
onChange={this.handleResizeSizeChange}
|
||||
value={this.state.resizeSize}
|
||||
/>
|
||||
<span>px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"pull-right " + (!this.state.editing ? "" : "hide")}>
|
||||
<button type="submit" className="btn btn-primary btn-sm glyphicon glyphicon-pencil" onClick={this.edit}></button>
|
||||
</div>
|
||||
<p className={"header " + (!this.state.editing ? "" : "hide")}><strong>Thank you!</strong> Please wait for the upload to complete.</p>
|
||||
{this.state.editTaskFormLoaded ?
|
||||
<div className="form-group">
|
||||
<div className="col-sm-offset-2 col-sm-10 text-right">
|
||||
<button type="submit" className="btn btn-primary" onClick={this.save}><i className="glyphicon glyphicon-saved"></i> Start Processing</button>
|
||||
</div>
|
||||
</div>
|
||||
: ""}
|
||||
</div>
|
||||
);
|
||||
}else{
|
||||
return (<div><i className="fa fa-refresh fa-spin fa-fw"></i></div>);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class ProgressBar extends React.Component {
|
||||
static propTypes = {
|
||||
current: PropTypes.number.isRequired, // processed files so far
|
||||
total: PropTypes.number.isRequired, // number of files
|
||||
template: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.func
|
||||
]).isRequired // String or callback for label
|
||||
}
|
||||
|
||||
render() {
|
||||
let { current, total } = this.props;
|
||||
if (total == 0) total = 1;
|
||||
|
||||
const percentage = ((current / total) * 100).toFixed(2);
|
||||
const active = percentage < 100 ? "active" : "";
|
||||
|
||||
const label = typeof this.props.template === 'string' ?
|
||||
this.props.template :
|
||||
this.props.template({current, total, active, percentage});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="progress">
|
||||
<div className={'progress-bar progress-bar-success progress-bar-striped ' + active} style={{width: percentage + '%'}}>
|
||||
{percentage}%
|
||||
</div>
|
||||
</div>
|
||||
{label !== "" ?
|
||||
<div className="text-left small">
|
||||
{label}
|
||||
</div>
|
||||
: ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProgressBar;
|
|
@ -4,6 +4,7 @@ import update from 'immutability-helper';
|
|||
import TaskList from './TaskList';
|
||||
import NewTaskPanel from './NewTaskPanel';
|
||||
import UploadProgressBar from './UploadProgressBar';
|
||||
import ProgressBar from './ProgressBar';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
import EditProjectDialog from './EditProjectDialog';
|
||||
import Dropzone from '../vendor/dropzone';
|
||||
|
@ -26,7 +27,6 @@ class ProjectListItem extends React.Component {
|
|||
|
||||
this.state = {
|
||||
showTaskList: this.historyNav.isValueInQSList("project_task_open", props.data.id),
|
||||
updatingTask: false,
|
||||
upload: this.getDefaultUploadState(),
|
||||
error: "",
|
||||
data: props.data,
|
||||
|
@ -64,7 +64,6 @@ class ProjectListItem extends React.Component {
|
|||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
if (this.updateTaskRequest) this.updateTaskRequest.abort();
|
||||
if (this.deleteProjectRequest) this.deleteProjectRequest.abort();
|
||||
if (this.refreshRequest) this.refreshRequest.abort();
|
||||
}
|
||||
|
@ -73,13 +72,13 @@ class ProjectListItem extends React.Component {
|
|||
return {
|
||||
uploading: false,
|
||||
editing: false,
|
||||
resizing: false,
|
||||
resizedImages: 0,
|
||||
error: "",
|
||||
progress: 0,
|
||||
totalCount: 0,
|
||||
totalBytes: 0,
|
||||
totalBytesSent: 0,
|
||||
savedTaskInfo: false,
|
||||
taskId: null
|
||||
totalBytesSent: 0
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -115,10 +114,7 @@ class ProjectListItem extends React.Component {
|
|||
|
||||
headers: {
|
||||
[csrf.header]: csrf.token
|
||||
},
|
||||
|
||||
resizeWidth: 2048,
|
||||
resizeQuality: 1.0
|
||||
}
|
||||
});
|
||||
|
||||
this.dz.on("totaluploadprogress", (progress, totalBytes, totalBytesSent) => {
|
||||
|
@ -128,28 +124,15 @@ class ProjectListItem extends React.Component {
|
|||
})
|
||||
.on("addedfiles", files => {
|
||||
this.setUploadState({
|
||||
editing: true,
|
||||
totalCount: files.length
|
||||
});
|
||||
|
||||
// TODO: ask for image resize
|
||||
|
||||
this.dz.processQueue();
|
||||
})
|
||||
.on("transformstart", () => {
|
||||
console.log("START");
|
||||
})
|
||||
.on("transformcompleted", (n) => {
|
||||
console.log(n);
|
||||
// TODO: update state
|
||||
.on("transformcompleted", (total) => {
|
||||
this.setUploadState({resizedImages: total});
|
||||
})
|
||||
.on("transformend", () => {
|
||||
console.log("END");
|
||||
})
|
||||
.on("processingmultiple", () => {
|
||||
this.setUploadState({
|
||||
uploading: true,
|
||||
editing: true
|
||||
})
|
||||
this.setUploadState({resizing: false, uploading: true});
|
||||
})
|
||||
.on("completemultiple", (files) => {
|
||||
// Check
|
||||
|
@ -158,24 +141,20 @@ class ProjectListItem extends React.Component {
|
|||
// All files have uploaded!
|
||||
if (success){
|
||||
this.setUploadState({uploading: false});
|
||||
|
||||
try{
|
||||
let response = JSON.parse(files[0].xhr.response);
|
||||
if (!response.id) throw new Error(`Expected id field, but none given (${response})`);
|
||||
|
||||
let taskId = response.id;
|
||||
this.setUploadState({taskId});
|
||||
|
||||
// Update task information (if the user has completed this step)
|
||||
if (this.state.upload.savedTaskInfo){
|
||||
this.updateTaskInfo(taskId, this.newTaskPanel.getTaskInfo());
|
||||
if (this.state.showTaskList){
|
||||
this.taskList.refresh();
|
||||
}else{
|
||||
// Need to wait for user to confirm task options
|
||||
this.setState({showTaskList: true});
|
||||
}
|
||||
this.resetUploadState();
|
||||
this.refresh();
|
||||
}catch(e){
|
||||
this.setUploadState({error: `Invalid response from server: ${e.message}`})
|
||||
this.setUploadState({error: `Invalid response from server: ${e.message}`, uploading: false})
|
||||
}
|
||||
|
||||
}else{
|
||||
this.setUploadState({
|
||||
uploading: false,
|
||||
|
@ -187,49 +166,21 @@ class ProjectListItem extends React.Component {
|
|||
this.resetUploadState();
|
||||
})
|
||||
.on("dragenter", () => {
|
||||
this.resetUploadState();
|
||||
if (!this.state.upload.uploading && !this.state.upload.resizing){
|
||||
this.resetUploadState();
|
||||
}
|
||||
})
|
||||
.on("sending", (file, xhr, formData) => {
|
||||
if (!formData.has("auto_processing_node")){
|
||||
formData.append('auto_processing_node', "false");
|
||||
}
|
||||
const taskInfo = this.dz._taskInfo;
|
||||
|
||||
if (!formData.has("name")) formData.append("name", taskInfo.name);
|
||||
if (!formData.has("options")) formData.append("options", JSON.stringify(taskInfo.options));
|
||||
if (!formData.has("processing_node")) formData.append("processing_node", taskInfo.selectedNode.id);
|
||||
if (!formData.has("auto_processing_node")) formData.append("auto_processing_node", taskInfo.selectedNode.key == "auto");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateTaskInfo(taskId, taskInfo){
|
||||
if (!taskId) throw new Error("taskId is not set");
|
||||
if (!taskInfo) throw new Error("taskId is not set");
|
||||
|
||||
this.setUploadState({editing: false});
|
||||
this.setState({updatingTask: true});
|
||||
|
||||
this.updateTaskRequest =
|
||||
$.ajax({
|
||||
url: `/api/projects/${this.state.data.id}/tasks/${this.state.upload.taskId}/`,
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
name: taskInfo.name,
|
||||
options: taskInfo.options,
|
||||
processing_node: taskInfo.selectedNode.id,
|
||||
auto_processing_node: taskInfo.selectedNode.key == "auto"
|
||||
}),
|
||||
dataType: 'json',
|
||||
type: 'PATCH'
|
||||
}).done((json) => {
|
||||
if (this.state.showTaskList){
|
||||
this.taskList.refresh();
|
||||
}else{
|
||||
this.setState({showTaskList: true});
|
||||
}
|
||||
this.refresh();
|
||||
}).fail(() => {
|
||||
this.setUploadState({error: "Could not update task information. Plese try again."});
|
||||
}).always(() => {
|
||||
this.setState({updatingTask: false});
|
||||
});
|
||||
}
|
||||
|
||||
setRef(prop){
|
||||
return (domNode) => {
|
||||
if (domNode != null) this[prop] = domNode;
|
||||
|
@ -272,12 +223,19 @@ class ProjectListItem extends React.Component {
|
|||
}
|
||||
|
||||
handleTaskSaved(taskInfo){
|
||||
this.setUploadState({savedTaskInfo: true});
|
||||
this.dz._taskInfo = taskInfo; // Allow us to access the task info from dz
|
||||
|
||||
// Has the upload finished?
|
||||
if (!this.state.upload.uploading && this.state.upload.taskId !== null){
|
||||
this.updateTaskInfo(this.state.upload.taskId, taskInfo);
|
||||
// Update dropzone settings
|
||||
if (taskInfo.resizeTo !== null){
|
||||
this.dz.options.resizeWidth = taskInfo.resizeTo;
|
||||
this.dz.options.resizeQuality = 1.0;
|
||||
|
||||
this.setUploadState({resizing: true, editing: false});
|
||||
}else{
|
||||
this.setUploadState({uploading: true, editing: false});
|
||||
}
|
||||
|
||||
this.dz.processQueue();
|
||||
}
|
||||
|
||||
handleEditProject(){
|
||||
|
@ -374,7 +332,18 @@ class ProjectListItem extends React.Component {
|
|||
</div>
|
||||
<i className="drag-drop-icon fa fa-inbox"></i>
|
||||
<div className="row">
|
||||
{this.state.upload.editing ? <UploadProgressBar {...this.state.upload}/> : ""}
|
||||
{this.state.upload.uploading ? <UploadProgressBar {...this.state.upload}/> : ""}
|
||||
{this.state.upload.resizing ?
|
||||
<ProgressBar
|
||||
current={this.state.upload.resizedImages}
|
||||
total={this.state.upload.totalCount}
|
||||
template={(info) => `Resized ${info.current} of ${info.total} images. Your browser might slow down during this process.`}
|
||||
/>
|
||||
: ""}
|
||||
|
||||
{this.state.upload.uploading || this.state.upload.resizing ?
|
||||
<i className="fa fa-refresh fa-spin fa-fw" />
|
||||
: ""}
|
||||
|
||||
{this.state.upload.error !== "" ?
|
||||
<div className="alert alert-warning alert-dismissible">
|
||||
|
@ -384,17 +353,13 @@ class ProjectListItem extends React.Component {
|
|||
: ""}
|
||||
|
||||
{this.state.upload.editing ?
|
||||
<NewTaskPanel
|
||||
uploading={this.state.upload.uploading}
|
||||
<NewTaskPanel
|
||||
onSave={this.handleTaskSaved}
|
||||
ref={this.setRef("newTaskPanel")}
|
||||
filesCount={this.state.upload.totalCount}
|
||||
showResize={true}
|
||||
/>
|
||||
: ""}
|
||||
|
||||
{this.state.updatingTask ?
|
||||
<span>Updating task information... <i className="fa fa-refresh fa-spin fa-fw"></i></span>
|
||||
: ""}
|
||||
|
||||
{this.state.showTaskList ?
|
||||
<TaskList
|
||||
ref={this.setRef("taskList")}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import ProgressBar from '../ProgressBar';
|
||||
|
||||
describe('<ProgressBar />', () => {
|
||||
it('renders without exploding', () => {
|
||||
const wrapper = shallow(<ProgressBar current={2} total={100} template="Hello" />);
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
})
|
||||
});
|
|
@ -17,4 +17,23 @@
|
|||
p.header{
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.resize-control{
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 8px;
|
||||
width: 11em;
|
||||
|
||||
input{
|
||||
width: 8em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
span{
|
||||
width: 2em;
|
||||
opacity: 0.6;
|
||||
font-size: 90%;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2493,21 +2493,29 @@ var Dropzone = function (_Emitter) {
|
|||
// Clumsy way of handling asynchronous calls, until I get to add a proper Future library.
|
||||
var doneCounter = 0;
|
||||
|
||||
// Modified for WebODM
|
||||
_this17.emit("transformstart", files);
|
||||
|
||||
var _loop = function _loop(i) {
|
||||
_this17.options.transformFile.call(_this17, files[i], function (transformedFile) {
|
||||
transformedFiles[i] = transformedFile;
|
||||
_this17.emit("transformcompleted", files.length);
|
||||
if (++doneCounter === files.length) {
|
||||
_this17.emit("transformend", files);
|
||||
done(transformedFiles);
|
||||
}
|
||||
});
|
||||
};
|
||||
// Process in batches based on the available number of cores
|
||||
var stride = Math.max(1, (navigator.hardwareConcurrency || 2) - 1);
|
||||
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
_loop(i);
|
||||
var process = function(i, s){
|
||||
if (files[i + s]){
|
||||
_this17.options.transformFile.call(_this17, files[i + s], function (transformedFile) {
|
||||
transformedFiles[i + s] = transformedFile;
|
||||
_this17.emit("transformcompleted", doneCounter + 1);
|
||||
if (++doneCounter === files.length) {
|
||||
_this17.emit("transformend", files);
|
||||
done(transformedFiles);
|
||||
}else{
|
||||
process(i + stride, s);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (var s = 0; s < stride; s++){
|
||||
process(0, s);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue