blob: 05b254d9d91c97b5df7b7ad700a881a56e079c1c [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Controler for handling behaviors of Files.app opened as a file/folder
* selection dialog.
*
* @param {DialogType} dialogType Dialog type.
* @param {!DialogFooter} dialogFooter Dialog footer.
* @param {!DirectoryModel} directoryModel Directory model.
* @param {!MetadataCache} metadataCache Metadata cache.
* @param {!NamingController} namingController Naming controller.
* @param {boolean} shouldReturnLocalPath Whether the dialog should return local
* path or not.
* @constructor
* @struct
*/
function DialogActionController(
dialogType,
dialogFooter,
directoryModel,
metadataCache,
namingController,
shouldReturnLocalPath) {
/**
* @type {!DialogType}
* @const
* @private
*/
this.dialogType_ = dialogType;
/**
* @type {!DialogFooter}
* @const
* @private
*/
this.dialogFooter_ = dialogFooter;
/**
* @type {!DirectoryModel}
* @const
* @private
*/
this.directoryModel_ = directoryModel;
/**
* @type {!MetadataCache}
* @const
* @private
*/
this.metadataCache_ = metadataCache;
/**
* @type {!NamingController}
* @const
* @private
*/
this.namingController_ = namingController;
/**
* @type {boolean}
* @const
* @private
*/
this.shouldReturnLocalPath_ = shouldReturnLocalPath;
/**
* Bound function for onCancel_.
* @type {!function(this:DialogActionController, Event)}
* @private
*/
this.onCancelBound_ = this.processCancelAction_.bind(this);
dialogFooter.okButton.addEventListener(
'click', this.processOKAction_.bind(this));
dialogFooter.cancelButton.addEventListener(
'click', this.onCancelBound_);
}
/**
* @private
*/
DialogActionController.prototype.processOKActionForSaveDialog_ = function() {
// Save-as doesn't require a valid selection from the list, since
// we're going to take the filename from the text input.
var filename = this.dialogFooter_.filenameInput.value;
if (!filename)
throw new Error('Missing filename!');
this.namingController_.validateFileNameForSaving(filename).then(
function(url) {
// TODO(mtomasz): Clean this up by avoiding constructing a URL
// via string concatenation.
this.selectFilesAndClose_({
urls: [url],
multiple: false,
filterIndex: this.dialogFooter_.selectedFilterIndex
});
}.bind(this)).catch(function(error) {
if (error instanceof Error)
console.error(error.stack && error);
});
};
/**
* Handle a click of the ok button.
*
* The ok button has different UI labels depending on the type of dialog, but
* in code it's always referred to as 'ok'.
*
* @private
*/
DialogActionController.prototype.processOKAction_ = function() {
if (this.dialogType_ == DialogType.SELECT_SAVEAS_FILE) {
this.processOKActionForSaveDialog_();
return;
}
var files = [];
var selectedIndexes =
this.directoryModel_.getFileListSelection().selectedIndexes;
if (DialogType.isFolderDialog(this.dialogType_) &&
selectedIndexes.length == 0) {
var url = this.directoryModel_.getCurrentDirEntry().toURL();
var singleSelection = {
urls: [url],
multiple: false,
filterIndex: this.dialogFooter_.selectedFilterIndex
};
this.selectFilesAndClose_(singleSelection);
return;
}
// All other dialog types require at least one selected list item.
// The logic to control whether or not the ok button is enabled should
// prevent us from ever getting here, but we sanity check to be sure.
if (!selectedIndexes.length)
throw new Error('Nothing selected!');
var dm = this.directoryModel_.getFileList();
for (var i = 0; i < selectedIndexes.length; i++) {
var entry = dm.item(selectedIndexes[i]);
if (!entry) {
console.error('Error locating selected file at index: ' + i);
continue;
}
files.push(entry.toURL());
}
// Multi-file selection has no other restrictions.
if (this.dialogType_ == DialogType.SELECT_OPEN_MULTI_FILE) {
var multipleSelection = {
urls: files,
multiple: true
};
this.selectFilesAndClose_(multipleSelection);
return;
}
// Everything else must have exactly one.
if (files.length > 1)
throw new Error('Too many files selected!');
var selectedEntry = dm.item(selectedIndexes[0]);
if (DialogType.isFolderDialog(this.dialogType_)) {
if (!selectedEntry.isDirectory)
throw new Error('Selected entry is not a folder!');
} else if (this.dialogType_ == DialogType.SELECT_OPEN_FILE) {
if (!selectedEntry.isFile)
throw new Error('Selected entry is not a file!');
}
var singleSelection = {
urls: [files[0]],
multiple: false,
filterIndex: this.dialogFooter_.selectedFilterIndex
};
this.selectFilesAndClose_(singleSelection);
};
/**
* Cancels file selection and closes the file selection dialog.
* @private
*/
DialogActionController.prototype.processCancelAction_ = function() {
chrome.fileManagerPrivate.cancelDialog();
window.close();
};
/**
* Tries to close this modal dialog with some files selected.
* Performs preprocessing if needed (e.g. for Drive).
* @param {Object} selection Contains urls, filterIndex and multiple fields.
* @private
*/
DialogActionController.prototype.selectFilesAndClose_ = function(selection) {
var callSelectFilesApiAndClose = function(callback) {
var onFileSelected = function() {
callback();
if (!chrome.runtime.lastError) {
// Call next method on a timeout, as it's unsafe to
// close a window from a callback.
setTimeout(window.close.bind(window), 0);
}
};
if (selection.multiple) {
chrome.fileManagerPrivate.selectFiles(
selection.urls,
this.shouldReturnLocalPath_,
onFileSelected);
} else {
chrome.fileManagerPrivate.selectFile(
selection.urls[0],
selection.filterIndex,
this.dialogType_ != DialogType.SELECT_SAVEAS_FILE /* for opening */,
this.shouldReturnLocalPath_,
onFileSelected);
}
}.bind(this);
var currentRootType = this.directoryModel_.getCurrentRootType();
var currentVolumeType = currentRootType != null ?
VolumeManagerCommon.getVolumeTypeFromRootType(currentRootType) : null;
if (currentRootType != VolumeManagerCommon.VolumeType.DRIVE ||
this.dialogType_ == DialogType.SELECT_SAVEAS_FILE) {
callSelectFilesApiAndClose(function() {});
return;
}
var shade = document.createElement('div');
shade.className = 'shade';
var footer = this.dialogFooter_.element;
var progress = footer.querySelector('.progress-track');
progress.style.width = '0%';
var cancelled = false;
var progressMap = {};
var filesStarted = 0;
var filesTotal = selection.urls.length;
for (var index = 0; index < selection.urls.length; index++) {
progressMap[selection.urls[index]] = -1;
}
var lastPercent = 0;
var bytesTotal = 0;
var bytesDone = 0;
var onFileTransfersUpdated = function(status) {
if (!(status.fileUrl in progressMap))
return;
if (status.total == -1)
return;
var old = progressMap[status.fileUrl];
if (old == -1) {
// -1 means we don't know file size yet.
bytesTotal += status.total;
filesStarted++;
old = 0;
}
bytesDone += status.processed - old;
progressMap[status.fileUrl] = status.processed;
var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal;
// For files we don't have information about, assume the progress is zero.
percent = percent * filesStarted / filesTotal * 100;
// Do not decrease the progress. This may happen, if first downloaded
// file is small, and the second one is large.
lastPercent = Math.max(lastPercent, percent);
progress.style.width = lastPercent + '%';
}.bind(this);
var setup = function() {
document.querySelector('.dialog-container').appendChild(shade);
setTimeout(function() { shade.setAttribute('fadein', 'fadein'); }, 100);
footer.setAttribute('progress', 'progress');
this.dialogFooter_.cancelButton.removeEventListener(
'click', this.onCancelBound_);
this.dialogFooter_.cancelButton.addEventListener('click', onCancel);
chrome.fileManagerPrivate.onFileTransfersUpdated.addListener(
onFileTransfersUpdated);
}.bind(this);
var cleanup = function() {
shade.parentNode.removeChild(shade);
footer.removeAttribute('progress');
this.dialogFooter_.cancelButton.removeEventListener('click', onCancel);
this.dialogFooter_.cancelButton.addEventListener(
'click', this.onCancelBound_);
chrome.fileManagerPrivate.onFileTransfersUpdated.removeListener(
onFileTransfersUpdated);
}.bind(this);
var onCancel = function() {
// According to API cancel may fail, but there is no proper UI to reflect
// this. So, we just silently assume that everything is cancelled.
chrome.fileManagerPrivate.cancelFileTransfers(
selection.urls, function(response) {});
}.bind(this);
var onProperties = function(properties) {
for (var i = 0; i < properties.length; i++) {
if (!properties[i] || properties[i].present) {
// For files already in GCache, we don't get any transfer updates.
filesTotal--;
}
}
callSelectFilesApiAndClose(cleanup);
}.bind(this);
setup();
// TODO(mtomasz): Use Entry instead of URLs, if possible.
util.URLsToEntries(selection.urls, function(entries) {
this.metadataCache_.get(entries, 'external', onProperties);
}.bind(this));
};