| // Copyright (c) 2013 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() { |
| ActionChoice.load(); |
| }); |
| |
| /** |
| * The main ActionChoice object. |
| * |
| * @param {HTMLElement} dom Container. |
| * @param {Object} params Parameters. |
| * @constructor |
| */ |
| function ActionChoice(dom, params) { |
| this.dom_ = dom; |
| this.params_ = params; |
| this.document_ = this.dom_.ownerDocument; |
| this.metadataCache_ = this.params_.metadataCache; |
| this.volumeManager_ = new VolumeManagerWrapper( |
| VolumeManagerWrapper.DriveEnabledStatus.DRIVE_ENABLED); |
| this.volumeManager_.addEventListener('externally-unmounted', |
| this.onDeviceUnmounted_.bind(this)); |
| this.initDom_(); |
| |
| // Load defined actions and remembered choice, then initialize volumes. |
| this.actions_ = []; |
| this.actionsById_ = {}; |
| this.rememberedChoice_ = null; |
| |
| ActionChoiceUtil.getDefinedActions(loadTimeData, function(actions) { |
| for (var i = 0; i < actions.length; i++) { |
| this.registerAction_(actions[i]); |
| } |
| |
| this.viewFilesAction_ = this.actionsById_['view-files']; |
| this.importPhotosToDriveAction_ = |
| this.actionsById_['import-photos-to-drive']; |
| this.watchSingleVideoAction_ = |
| this.actionsById_['watch-single-video']; |
| |
| // Special case: if Google+ Photos is installed, then do not show Drive. |
| for (var i = 0; i < actions.length; i++) { |
| if (actions[i].extensionId == ActionChoice.GPLUS_PHOTOS_EXTENSION_ID) { |
| this.importPhotosToDriveAction_.hidden = true; |
| break; |
| } |
| } |
| |
| if (this.params_.advancedMode) { |
| // In the advanced mode, skip auto-choice. |
| this.initializeVolumes_(); |
| } else { |
| // Get the remembered action before initializing volumes. |
| ActionChoiceUtil.getRememberedActionId(function(actionId) { |
| this.rememberedChoice_ = actionId; |
| this.initializeVolumes_(); |
| }.bind(this)); |
| } |
| this.renderList_(); |
| }.bind(this)); |
| |
| // Try to render, what is already available. |
| this.renderList_(); |
| } |
| |
| ActionChoice.prototype = { __proto__: cr.EventTarget.prototype }; |
| |
| /** |
| * The number of previews shown. |
| * @type {number} |
| * @const |
| */ |
| ActionChoice.PREVIEW_COUNT = 3; |
| |
| /** |
| * Extension id of Google+ Photos app. |
| * @type {string} |
| * @const |
| */ |
| ActionChoice.GPLUS_PHOTOS_EXTENSION_ID = 'efjnaogkjbogokcnohkmnjdojkikgobo'; |
| |
| /** |
| * Loads app in the document body. |
| * @param {Object=} opt_params Parameters. |
| */ |
| ActionChoice.load = function(opt_params) { |
| ImageUtil.metrics = metrics; |
| |
| var hash = location.hash ? decodeURIComponent(location.hash.substr(1)) : ''; |
| var query = |
| location.search ? decodeURIComponent(location.search.substr(1)) : ''; |
| var params = opt_params || {}; |
| if (!params.source) params.source = hash; |
| if (!params.advancedMode) params.advancedMode = (query == 'advanced-mode'); |
| if (!params.metadataCache) params.metadataCache = MetadataCache.createFull(); |
| |
| chrome.fileBrowserPrivate.getStrings(function(strings) { |
| loadTimeData.data = strings; |
| i18nTemplate.process(document, loadTimeData); |
| var dom = document.querySelector('.action-choice'); |
| ActionChoice.instance = new ActionChoice(dom, params); |
| }); |
| }; |
| |
| /** |
| * Registers an action. |
| * @param {Object} action Action item. |
| * @private |
| */ |
| ActionChoice.prototype.registerAction_ = function(action) { |
| this.actions_.push(action); |
| this.actionsById_[action.id] = action; |
| }; |
| |
| /** |
| * Initializes the source and Drive. If the remembered choice is available, |
| * then performs the action. |
| * @private |
| */ |
| ActionChoice.prototype.initializeVolumes_ = function() { |
| var checkDriveFinished = false; |
| var loadSourceFinished = false; |
| |
| var maybeRunRememberedAction = function() { |
| if (!checkDriveFinished || !loadSourceFinished) |
| return; |
| |
| // Run the remembered action if it is available. |
| if (this.rememberedChoice_) { |
| var action = this.actionsById_[this.rememberedChoice_]; |
| if (action && !action.disabled) |
| this.runAction_(action); |
| } |
| }.bind(this); |
| |
| var onCheckDriveFinished = function() { |
| checkDriveFinished = true; |
| maybeRunRememberedAction(); |
| }; |
| |
| var onLoadSourceFinished = function() { |
| loadSourceFinished = true; |
| maybeRunRememberedAction(); |
| }; |
| |
| this.checkDrive_(onCheckDriveFinished); |
| this.loadSource_(this.params_.source, onLoadSourceFinished); |
| }; |
| |
| /** |
| * One-time initialization of dom elements. |
| * @private |
| */ |
| ActionChoice.prototype.initDom_ = function() { |
| this.list_ = new cr.ui.List(); |
| this.list_.id = 'actions-list'; |
| this.document_.querySelector('.choices').appendChild(this.list_); |
| |
| var self = this; // .bind(this) doesn't work on constructors. |
| this.list_.itemConstructor = function(item) { |
| return self.renderItem(item); |
| }; |
| |
| this.list_.selectionModel = new cr.ui.ListSingleSelectionModel(); |
| this.list_.dataModel = new cr.ui.ArrayDataModel([]); |
| this.list_.autoExpands = true; |
| |
| var acceptActionBound = function() { |
| this.acceptAction_(); |
| }.bind(this); |
| this.list_.activateItemAtIndex = acceptActionBound; |
| this.list_.addEventListener('click', acceptActionBound); |
| |
| this.previews_ = this.document_.querySelector('.previews'); |
| this.counter_ = this.document_.querySelector('.counter'); |
| this.document_.addEventListener('keydown', this.onKeyDown_.bind(this)); |
| |
| metrics.startInterval('PhotoImport.Load'); |
| this.dom_.setAttribute('loading', ''); |
| }; |
| |
| /** |
| * Renders the list. |
| * @private |
| */ |
| ActionChoice.prototype.renderList_ = function() { |
| var currentItem = this.list_.dataModel.item( |
| this.list_.selectionModel.selectedIndex); |
| |
| this.list_.startBatchUpdates(); |
| this.list_.dataModel.splice(0, this.list_.dataModel.length); |
| |
| for (var i = 0; i < this.actions_.length; i++) { |
| if (!this.actions_[i].hidden) |
| this.list_.dataModel.push(this.actions_[i]); |
| } |
| |
| for (var i = 0; i < this.list_.dataModel.length; i++) { |
| if (this.list_.dataModel.item(i) == currentItem) { |
| this.list_.selectionModel.selectedIndex = i; |
| break; |
| } |
| } |
| |
| this.list_.endBatchUpdates(); |
| }; |
| |
| /** |
| * Renders an item in the list. |
| * @param {Object} item Item to render. |
| * @return {Element} DOM element with representing the item. |
| */ |
| ActionChoice.prototype.renderItem = function(item) { |
| var result = this.document_.createElement('li'); |
| |
| var div = this.document_.createElement('div'); |
| if (item.disabled && item.disabledTitle) |
| div.textContent = item.disabledTitle; |
| else |
| div.textContent = item.title; |
| |
| if (item.class) |
| div.classList.add(item.class); |
| if (item.icon100 && item.icon200) |
| div.style.backgroundImage = '-webkit-image-set(' + |
| 'url(' + item.icon100 + ') 1x,' + |
| 'url(' + item.icon200 + ') 2x)'; |
| if (item.disabled) |
| div.classList.add('disabled'); |
| |
| cr.defineProperty(result, 'lead', cr.PropertyKind.BOOL_ATTR); |
| cr.defineProperty(result, 'selected', cr.PropertyKind.BOOL_ATTR); |
| result.appendChild(div); |
| |
| return result; |
| }; |
| |
| /** |
| * Checks whether Drive is reachable. |
| * |
| * @param {function()} callback Completion callback. |
| * @private |
| */ |
| ActionChoice.prototype.checkDrive_ = function(callback) { |
| this.volumeManager_.ensureInitialized(function() { |
| this.importPhotosToDriveAction_.disabled = |
| !this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE); |
| this.renderList_(); |
| callback(); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Load the source contents. |
| * |
| * @param {string} source Path to source. |
| * @param {function()} callback Completion callback. |
| * @private |
| */ |
| ActionChoice.prototype.loadSource_ = function(source, callback) { |
| var onTraversed = function(results) { |
| metrics.recordInterval('PhotoImport.Scan'); |
| var videos = results.filter(FileType.isVideo); |
| if (videos.length == 1) { |
| this.singleVideo_ = videos[0]; |
| this.watchSingleVideoAction_.title = loadTimeData.getStringF( |
| 'ACTION_CHOICE_WATCH_SINGLE_VIDEO', videos[0].name); |
| this.watchSingleVideoAction_.hidden = false; |
| this.watchSingleVideoAction_.disabled = false; |
| this.renderList_(); |
| } |
| |
| var mediaFiles = results.filter(FileType.isImageOrVideo); |
| if (mediaFiles.length == 0) { |
| // If we have no media files, the only choice is view files. So, don't |
| // confuse user with a single choice, and just open file manager. |
| this.viewFiles_(); |
| this.recordAction_('view-files-auto'); |
| this.close_(); |
| } |
| |
| if (mediaFiles.length < ActionChoice.PREVIEW_COUNT) { |
| this.counter_.textContent = loadTimeData.getStringF( |
| 'ACTION_CHOICE_COUNTER_NO_MEDIA', results.length); |
| } else { |
| this.counter_.textContent = loadTimeData.getStringF( |
| 'ACTION_CHOICE_COUNTER', mediaFiles.length); |
| } |
| var previews = mediaFiles.length ? mediaFiles : results; |
| var previewsCount = Math.min(ActionChoice.PREVIEW_COUNT, previews.length); |
| this.renderPreview_(previews, previewsCount); |
| callback(); |
| }.bind(this); |
| |
| var onEntry = function(entry) { |
| this.sourceEntry_ = entry; |
| this.document_.querySelector('title').textContent = entry.name; |
| |
| var volumeInfo = this.volumeManager_.getVolumeInfo(entry.fullPath); |
| var deviceType = volumeInfo && volumeInfo.deviceType; |
| if (deviceType != 'sd') deviceType = 'usb'; |
| this.dom_.querySelector('.device-type').setAttribute('device-type', |
| deviceType); |
| this.dom_.querySelector('.loading-text').textContent = |
| loadTimeData.getString('ACTION_CHOICE_LOADING_' + |
| deviceType.toUpperCase()); |
| |
| var entryList = []; |
| util.traverseTree( |
| entry, |
| function(traversedEntry) { |
| if (!FileType.isVisible(traversedEntry)) |
| return false; |
| entryList.push(traversedEntry); |
| return true; |
| }, |
| function() { |
| onTraversed(entryList); |
| }, |
| function(error) { |
| console.error( |
| 'Failed to traverse [' + entry.fullPath + ']: ' + error.code); |
| }); |
| }.bind(this); |
| |
| this.sourceEntry_ = null; |
| metrics.startInterval('PhotoImport.Scan'); |
| this.volumeManager_.ensureInitialized(function() { |
| this.volumeManager_.resolvePath( |
| source, onEntry, |
| function(error) { |
| this.recordAction_('error'); |
| this.close_(); |
| }.bind(this)); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Renders a preview for a media entry. |
| * @param {Array.<FileEntry>} entries The entries. |
| * @param {number} count Remaining count. |
| * @private |
| */ |
| ActionChoice.prototype.renderPreview_ = function(entries, count) { |
| var entry = entries.shift(); |
| var box = this.document_.createElement('div'); |
| box.className = 'img-container'; |
| |
| var done = function() { |
| this.dom_.removeAttribute('loading'); |
| metrics.recordInterval('PhotoImport.Load'); |
| }.bind(this); |
| |
| var onSuccess = function() { |
| this.previews_.appendChild(box); |
| if (--count == 0) { |
| done(); |
| } else { |
| this.renderPreview_(entries, count); |
| } |
| }.bind(this); |
| |
| var onError = function() { |
| if (entries.length == 0) { |
| // Append one image with generic thumbnail. |
| this.previews_.appendChild(box); |
| done(); |
| } else { |
| this.renderPreview_(entries, count); |
| } |
| }.bind(this); |
| |
| this.metadataCache_.get(entry, 'thumbnail|filesystem', |
| function(metadata) { |
| new ThumbnailLoader(entry.toURL(), |
| ThumbnailLoader.LoaderType.IMAGE, |
| metadata).load( |
| box, |
| ThumbnailLoader.FillMode.FILL, |
| ThumbnailLoader.OptimizationMode.NEVER_DISCARD, |
| onSuccess, |
| onError, |
| onError); |
| }); |
| }; |
| |
| /** |
| * Closes the window. |
| * @private |
| */ |
| ActionChoice.prototype.close_ = function() { |
| window.close(); |
| }; |
| |
| /** |
| * Keydown event handler. |
| * @param {Event} e The event. |
| * @private |
| */ |
| ActionChoice.prototype.onKeyDown_ = function(e) { |
| switch (util.getKeyModifiers(e) + e.keyCode) { |
| case '13': |
| this.acceptAction_(); |
| break; |
| case '27': |
| this.recordAction_('close'); |
| this.close_(); |
| break; |
| } |
| }; |
| |
| /** |
| * Runs an action. |
| * @param {Object} action Action item to perform. |
| * @private |
| */ |
| ActionChoice.prototype.runAction_ = function(action) { |
| // TODO(mtomasz): Remove these predefined actions in Apps v2. |
| if (action == this.importPhotosToDriveAction_) { |
| var url = chrome.runtime.getURL('photo_import.html') + |
| '#' + this.sourceEntry_.fullPath; |
| var width = 728; |
| var height = 656; |
| var top = Math.round((window.screen.availHeight - height) / 2); |
| var left = Math.round((window.screen.availWidth - width) / 2); |
| chrome.app.window.create(url, |
| {height: height, width: width, left: left, top: top}); |
| this.recordAction_('import-photos-to-drive'); |
| this.close_(); |
| return; |
| } |
| |
| if (action == this.watchSingleVideoAction_) { |
| util.viewFilesInBrowser([this.singleVideo_.toURL()], |
| function(success) {}); |
| this.recordAction_('watch-single-video'); |
| this.close_(); |
| return; |
| } |
| |
| if (action == this.viewFilesAction_) { |
| this.viewFiles_(); |
| this.recordAction_('view-files'); |
| this.close_(); |
| return; |
| } |
| |
| if (!action.extensionId) { |
| console.error('Unknown predefined action.'); |
| return; |
| } |
| |
| // Run the media galleries handler. |
| chrome.mediaGalleriesPrivate.launchHandler(action.extensionId, |
| action.actionId, |
| this.params_.source); |
| this.close_(); |
| }; |
| |
| /** |
| * Handles accepting an action. Checks if the action is available, remembers |
| * and runs it. |
| * @private |
| */ |
| ActionChoice.prototype.acceptAction_ = function() { |
| var action = |
| this.list_.dataModel.item(this.list_.selectionModel.selectedIndex); |
| if (!action || action.hidden || action.disabled) |
| return; |
| |
| this.runAction_(action); |
| ActionChoiceUtil.setRememberedActionId(action.id); |
| }; |
| |
| /** |
| * Called when some device is unmounted. |
| * @param {Event} event Event object. |
| * @private |
| */ |
| ActionChoice.prototype.onDeviceUnmounted_ = function(event) { |
| if (this.sourceEntry_ && event.mountPath == this.sourceEntry_.fullPath) |
| window.close(); |
| }; |
| |
| /** |
| * Perform the 'view files' action. |
| * @private |
| */ |
| ActionChoice.prototype.viewFiles_ = function() { |
| var path = this.sourceEntry_.fullPath; |
| chrome.runtime.getBackgroundPage(function(bg) { |
| bg.launchFileManager({defaultPath: path}); |
| }); |
| }; |
| |
| /** |
| * Records an action chosen. |
| * @param {string} action Action name. |
| * @private |
| */ |
| ActionChoice.prototype.recordAction_ = function(action) { |
| metrics.recordEnum('PhotoImport.Action', action, |
| ['import-photos-to-drive', |
| 'view-files', |
| 'view-files-auto', |
| 'watch-single-video', |
| 'error', |
| 'close']); |
| }; |
| |
| /** |
| * Called when the page is unloaded. |
| */ |
| ActionChoice.prototype.onUnload = function() { |
| this.volumeManager_.dispose(); |
| }; |
| |
| function unload() { |
| if (ActionChoice.instance) |
| ActionChoice.instance.onUnload(); |
| } |