blob: 346e13c01788b8dedd4c980d703ad0c35339ffcc [file] [log] [blame]
// Copyright (c) 2012 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.
'use strict';
document.addEventListener('DOMContentLoaded', function() {
PhotoImport.load();
});
/**
* The main Photo App object.
* @param {HTMLElement} dom Container.
* @param {VolumeManagerWrapper} volumeManager The initialized
* VolumeManagerWrapper instance.
* @param {Object} params Parameters.
* @constructor
*/
function PhotoImport(dom, volumeManager, params) {
this.dom_ = dom;
this.document_ = this.dom_.ownerDocument;
this.metadataCache_ = params.metadataCache;
this.volumeManager_ = volumeManager;
this.fileOperationManager_ = FileOperationManagerWrapper.getInstance();
this.mediaFilesList_ = null;
this.destination_ = null;
this.myPhotosDirectory_ = null;
this.parentWindowId_ = params.parentWindowId;
this.initDom_();
this.initMyPhotos_();
this.loadSource_(params.source);
}
PhotoImport.prototype = { __proto__: cr.EventTarget.prototype };
/**
* Single item width.
* Keep in sync with .grid-item rule in photo_import.css.
*/
PhotoImport.ITEM_WIDTH = 164 + 8;
/**
* Number of tries in creating a destination directory.
*/
PhotoImport.CREATE_DESTINATION_TRIES = 100;
/**
* Loads app in the document body.
* @param {Object=} opt_params Parameters.
*/
PhotoImport.load = function(opt_params) {
ImageUtil.metrics = metrics;
var hash = location.hash ? location.hash.substr(1) : '';
var query = location.search ? location.search.substr(1) : '';
var params = opt_params || {};
if (!params.source) params.source = hash;
if (!params.parentWindowId && query) params.parentWindowId = query;
if (!params.metadataCache) params.metadataCache = MetadataCache.createFull();
var api = chrome.fileBrowserPrivate || window.top.chrome.fileBrowserPrivate;
api.getStrings(function(strings) {
loadTimeData.data = strings;
var dom = document.querySelector('.photo-import');
var volumeManager = new VolumeManagerWrapper(
VolumeManagerWrapper.DriveEnabledStatus.DRIVE_ENABLED);
volumeManager.ensureInitialized(function() {
new PhotoImport(dom, volumeManager, params);
});
});
};
/**
* One-time initialization of dom elements.
* @private
*/
PhotoImport.prototype.initDom_ = function() {
this.dom_.setAttribute('loading', '');
this.dom_.ownerDocument.defaultView.addEventListener(
'resize', this.onResize_.bind(this));
this.spinner_ = this.dom_.querySelector('.spinner');
this.document_.querySelector('title').textContent =
loadTimeData.getString('PHOTO_IMPORT_TITLE');
this.dom_.querySelector('.caption').textContent =
loadTimeData.getString('PHOTO_IMPORT_CAPTION');
this.selectAllNone_ = this.dom_.querySelector('.select');
this.selectAllNone_.addEventListener('click',
this.onSelectAllNone_.bind(this));
this.dom_.querySelector('label[for=delete-after-checkbox]').textContent =
loadTimeData.getString('PHOTO_IMPORT_DELETE_AFTER');
this.selectedCount_ = this.dom_.querySelector('.selected-count');
this.importButton_ = this.dom_.querySelector('button.import');
this.importButton_.textContent =
loadTimeData.getString('PHOTO_IMPORT_IMPORT_BUTTON');
this.importButton_.addEventListener('click', this.onImportClick_.bind(this));
this.cancelButton_ = this.dom_.querySelector('button.cancel');
this.cancelButton_.textContent = str('CANCEL_LABEL');
this.cancelButton_.addEventListener('click', this.onCancelClick_.bind(this));
this.grid_ = this.dom_.querySelector('grid');
cr.ui.Grid.decorate(this.grid_);
this.grid_.itemConstructor = GridItem.bind(null, this);
this.fileList_ = new cr.ui.ArrayDataModel([]);
this.grid_.selectionModel = new cr.ui.ListSelectionModel();
this.grid_.dataModel = this.fileList_;
this.grid_.selectionModel.addEventListener('change',
this.onSelectionChanged_.bind(this));
this.onSelectionChanged_();
this.importingDialog_ = new ImportingDialog(
this.dom_, this.fileOperationManager_,
this.metadataCache_, this.parentWindowId_);
var dialogs = cr.ui.dialogs;
dialogs.BaseDialog.OK_LABEL = str('OK_LABEL');
dialogs.BaseDialog.CANCEL_LABEL = str('CANCEL_LABEL');
this.alert_ = new dialogs.AlertDialog(this.dom_);
};
/**
* One-time initialization of the My Photos directory.
* @private
*/
PhotoImport.prototype.initMyPhotos_ = function() {
var driveVolume = this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE);
if (!driveVolume || driveVolume.error || !driveVolume.root) {
this.onError_(loadTimeData.getString('PHOTO_IMPORT_DRIVE_ERROR'));
return;
}
util.getOrCreateDirectory(
driveVolume.root,
loadTimeData.getString('PHOTO_IMPORT_MY_PHOTOS_DIRECTORY_NAME'),
function(entry) {
// This may enable the import button, so check that.
this.myPhotosDirectory_ = entry;
this.onSelectionChanged_();
},
function(error) {
this.onError_(loadTimeData.getString('PHOTO_IMPORT_DRIVE_ERROR'));
}.bind(this));
};
/**
* Creates the destination directory.
* @param {function} onSuccess Callback on success.
* @private
*/
PhotoImport.prototype.createDestination_ = function(onSuccess) {
var onError = this.onError_.bind(
this, loadTimeData.getString('PHOTO_IMPORT_DESTINATION_ERROR'));
var dateFormatter = Intl.DateTimeFormat(
[] /* default locale */,
{year: 'numeric', month: 'short', day: 'numeric'});
var baseName = PathUtil.join(
RootDirectory.DRIVE,
loadTimeData.getString('PHOTO_IMPORT_MY_PHOTOS_DIRECTORY_NAME'),
dateFormatter.format(new Date()));
var driveVolume = this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE);
if (!driveVolume || driveVolume.error || !driveVolume.root) {
onError();
return;
}
var tryNext = function(number) {
if (number > PhotoImport.CREATE_DESTINATION_TRIES) {
console.error('Too many directories with the same base name exist.');
onError();
return;
}
var directoryName = baseName;
if (number > 1)
directoryName += ' (' + (tryNumber) + ')';
driveVolume.root.getDirectory(
directoryName,
{create: true, exclusive: true},
function(entry) {
this.destination_ = entry;
onSuccess();
}.bind(this),
function(error) {
if (error.code === FileError.PATH_EXISTS_ERR) {
// If there already exists an entry, retry with incrementing the
// number.
tryNext(number + 1);
return;
}
onError();
}.bind(this));
}.bind(this);
tryNext(1);
};
/**
* Load the source contents.
* @param {string} source Path to source.
* @private
*/
PhotoImport.prototype.loadSource_ = function(source) {
var onError = this.onError_.bind(
this, loadTimeData.getString('PHOTO_IMPORT_SOURCE_ERROR'));
var result = [];
this.volumeManager_.resolvePath(
source,
function(sourceEntry) {
util.traverseTree(
entry,
function(entry) {
if (!FileType.isVisible(entry))
return false;
if (FileType.isImageOrVideo(entry))
result.push(entry);
return true;
},
function() {
this.dom_.removeAttribute('loading');
this.mediaFilesList_ = result;
this.fillGrid_();
}.bind(this),
onError);
}.bind(this),
onError);
};
/**
* Renders files into grid.
* @private
*/
PhotoImport.prototype.fillGrid_ = function() {
if (!this.mediaFilesList_) return;
this.fileList_.splice(0, this.fileList_.length);
this.fileList_.push.apply(this.fileList_, this.mediaFilesList_);
};
/**
* Creates groups for files based on modification date.
* @param {Array.<Entry>} files File list.
* @param {Object} filesystem Filesystem metadata.
* @return {Array.<Object>} List of grouped items.
* @private
*/
PhotoImport.prototype.createGroups_ = function(files, filesystem) {
var dateFormatter = Intl.DateTimeFormat(
[] /* default locale */,
{year: 'numeric', month: 'short', day: 'numeric'});
var columns = this.grid_.columns;
var unknownGroup = {
type: 'group',
date: 0,
title: loadTimeData.getString('PHOTO_IMPORT_UNKNOWN_DATE'),
items: []
};
var groupsMap = {};
for (var index = 0; index < files.length; index++) {
var props = filesystem[index];
var item = { type: 'entry', entry: files[index] };
if (!props || !props.modificationTime) {
item.group = unknownGroup;
unknownGroup.items.push(item);
continue;
}
var date = new Date(props.modificationTime);
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
var time = date.getTime();
if (!(time in groupsMap)) {
groupsMap[time] = {
type: 'group',
date: date,
title: dateFormatter.format(date),
items: []
};
}
var group = groupsMap[time];
group.items.push(item);
item.group = group;
}
var groups = [];
for (var time in groupsMap) {
if (groupsMap.hasOwnProperty(time)) {
groups.push(groupsMap[time]);
}
}
if (unknownGroup.items.length > 0)
groups.push(unknownGroup);
groups.sort(function(a, b) {
return b.date.getTime() - a.date.getTime();
});
var list = [];
for (var index = 0; index < groups.length; index++) {
var group = groups[index];
list.push(group);
for (var t = 1; t < columns; t++) {
list.push({ type: 'empty' });
}
for (var j = 0; j < group.items.length; j++) {
list.push(group.items[j]);
}
var count = group.items.length;
while (count % columns != 0) {
list.push({ type: 'empty' });
count++;
}
}
return list;
};
/**
* Decorates grid item.
* @param {HTMLLIElement} li The list item.
* @param {FileEntry} entry The file entry.
* @private
*/
PhotoImport.prototype.decorateGridItem_ = function(li, entry) {
li.className = 'grid-item';
li.entry = entry;
var frame = this.document_.createElement('div');
frame.className = 'grid-frame';
li.appendChild(frame);
var box = this.document_.createElement('div');
box.className = 'img-container';
this.metadataCache_.get(entry, 'thumbnail|filesystem',
function(metadata) {
new ThumbnailLoader(entry.toURL(),
ThumbnailLoader.LoaderType.IMAGE,
metadata).
load(box, ThumbnailLoader.FillMode.FIT,
ThumbnailLoader.OptimizationMode.DISCARD_DETACHED);
});
frame.appendChild(box);
var check = this.document_.createElement('div');
check.className = 'check';
li.appendChild(check);
};
/**
* Handles the 'pick all/none' action.
* @private
*/
PhotoImport.prototype.onSelectAllNone_ = function() {
var sm = this.grid_.selectionModel;
if (sm.selectedIndexes.length == this.fileList_.length) {
sm.unselectAll();
} else {
sm.selectAll();
}
};
/**
* Show error message.
* @param {string} message Error message.
* @private
*/
PhotoImport.prototype.onError_ = function(message) {
this.importingDialog_.hide(function() {
this.alert_.show(message,
function() {
window.close();
});
}.bind(this));
};
/**
* Resize event handler.
* @private
*/
PhotoImport.prototype.onResize_ = function() {
var g = this.grid_;
g.startBatchUpdates();
setTimeout(function() {
g.columns = 0;
g.redraw();
g.endBatchUpdates();
}, 0);
};
/**
* @return {Array.<Object>} The list of selected entries.
* @private
*/
PhotoImport.prototype.getSelectedItems_ = function() {
var indexes = this.grid_.selectionModel.selectedIndexes;
var list = [];
for (var i = 0; i < indexes.length; i++) {
list.push(this.fileList_.item(indexes[i]));
}
return list;
};
/**
* Event handler for picked items change.
* @private
*/
PhotoImport.prototype.onSelectionChanged_ = function() {
var count = this.grid_.selectionModel.selectedIndexes.length;
this.selectedCount_.textContent = count == 0 ? '' :
count == 1 ? loadTimeData.getString('PHOTO_IMPORT_ONE_SELECTED') :
loadTimeData.getStringF('PHOTO_IMPORT_MANY_SELECTED', count);
this.importButton_.disabled = count == 0 || this.myPhotosDirectory_ == null;
this.selectAllNone_.textContent = loadTimeData.getString(
count == this.fileList_.length && count > 0 ?
'PHOTO_IMPORT_SELECT_NONE' : 'PHOTO_IMPORT_SELECT_ALL');
};
/**
* Event handler for import button click.
* @param {Event} event The event.
* @private
*/
PhotoImport.prototype.onImportClick_ = function(event) {
var entries = this.getSelectedItems_();
var move = this.dom_.querySelector('#delete-after-checkbox').checked;
this.importingDialog_.show(entries, move);
this.createDestination_(function() {
var percentage = Math.round(entries.length / this.fileList_.length * 100);
metrics.recordMediumCount('PhotoImport.ImportCount', entries.length);
metrics.recordSmallCount('PhotoImport.ImportPercentage', percentage);
this.importingDialog_.start(this.destination_);
}.bind(this));
};
/**
* Click event handler for the cancel button.
* @param {Event} event The event.
* @private
*/
PhotoImport.prototype.onCancelClick_ = function(event) {
window.close();
};
/**
* Item in the grid.
* @param {PhotoImport} app Application instance.
* @param {Entry} entry File entry.
* @constructor
*/
function GridItem(app, entry) {
var li = app.document_.createElement('li');
li.__proto__ = GridItem.prototype;
app.decorateGridItem_(li, entry);
return li;
}
GridItem.prototype = {
__proto__: cr.ui.ListItem.prototype,
get label() {},
set label(value) {}
};
/**
* Creates a selection controller that is to be used with grid.
* @param {cr.ui.ListSelectionModel} selectionModel The selection model to
* interact with.
* @param {cr.ui.Grid} grid The grid to interact with.
* @constructor
* @extends {!cr.ui.ListSelectionController}
*/
function GridSelectionController(selectionModel, grid) {
this.selectionModel_ = selectionModel;
this.grid_ = grid;
}
/**
* Extends cr.ui.ListSelectionController.
*/
GridSelectionController.prototype.__proto__ =
cr.ui.ListSelectionController.prototype;
/** @override */
GridSelectionController.prototype.getIndexBelow = function(index) {
if (index == this.getLastIndex()) {
return -1;
}
var dm = this.grid_.dataModel;
var columns = this.grid_.columns;
var min = (Math.floor(index / columns) + 1) * columns;
for (var row = 1; true; row++) {
var end = index + columns * row;
var start = Math.max(min, index + columns * (row - 1));
if (start > dm.length) break;
for (var i = end; i > start; i--) {
if (i < dm.length && dm.item(i).type == 'entry')
return i;
}
}
return this.getLastIndex();
};
/** @override */
GridSelectionController.prototype.getIndexAbove = function(index) {
if (index == this.getFirstIndex()) {
return -1;
}
var dm = this.grid_.dataModel;
index -= this.grid_.columns;
while (index >= 0 && dm.item(index).type != 'entry') {
index--;
}
return index < 0 ? this.getFirstIndex() : index;
};
/** @override */
GridSelectionController.prototype.getIndexBefore = function(index) {
var dm = this.grid_.dataModel;
index--;
while (index >= 0 && dm.item(index).type != 'entry') {
index--;
}
return index;
};
/** @override */
GridSelectionController.prototype.getIndexAfter = function(index) {
var dm = this.grid_.dataModel;
index++;
while (index < dm.length && dm.item(index).type != 'entry') {
index++;
}
return index == dm.length ? -1 : index;
};
/** @override */
GridSelectionController.prototype.getFirstIndex = function() {
var dm = this.grid_.dataModel;
for (var index = 0; index < dm.length; index++) {
if (dm.item(index).type == 'entry')
return index;
}
return -1;
};
/** @override */
GridSelectionController.prototype.getLastIndex = function() {
var dm = this.grid_.dataModel;
for (var index = dm.length - 1; index >= 0; index--) {
if (dm.item(index).type == 'entry')
return index;
}
return -1;
};