| // 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; |
| }; |