1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 'use strict'; 6 7 document.addEventListener('DOMContentLoaded', function() { 8 ActionChoice.load(); 9 }); 10 11 /** 12 * The main ActionChoice object. 13 * 14 * @param {HTMLElement} dom Container. 15 * @param {FileSystem} filesystem Local file system. 16 * @param {Object} params Parameters. 17 * @constructor 18 */ 19 function ActionChoice(dom, filesystem, params) { 20 this.dom_ = dom; 21 this.filesystem_ = filesystem; 22 this.params_ = params; 23 this.document_ = this.dom_.ownerDocument; 24 this.metadataCache_ = this.params_.metadataCache; 25 this.volumeManager_ = VolumeManager.getInstance(); 26 this.volumeManager_.addEventListener('externally-unmounted', 27 this.onDeviceUnmounted_.bind(this)); 28 this.initDom_(); 29 30 // Load defined actions and remembered choice, then initialize volumes. 31 this.actions_ = []; 32 this.actionsById_ = {}; 33 this.rememberedChoice_ = null; 34 35 ActionChoiceUtil.getDefinedActions(loadTimeData, function(actions) { 36 for (var i = 0; i < actions.length; i++) { 37 this.registerAction_(actions[i]); 38 } 39 40 this.viewFilesAction_ = this.actionsById_['view-files']; 41 this.importPhotosToDriveAction_ = 42 this.actionsById_['import-photos-to-drive']; 43 this.watchSingleVideoAction_ = 44 this.actionsById_['watch-single-video']; 45 46 // Special case: if Google+ Photos is installed, then do not show Drive. 47 for (var i = 0; i < actions.length; i++) { 48 if (actions[i].extensionId == ActionChoice.GPLUS_PHOTOS_EXTENSION_ID) { 49 this.importPhotosToDriveAction_.hidden = true; 50 break; 51 } 52 } 53 54 if (this.params_.advancedMode) { 55 // In the advanced mode, skip auto-choice. 56 this.initializeVolumes_(); 57 } else { 58 // Get the remembered action before initializing volumes. 59 ActionChoiceUtil.getRememberedActionId(function(actionId) { 60 this.rememberedChoice_ = actionId; 61 this.initializeVolumes_(); 62 }.bind(this)); 63 } 64 this.renderList_(); 65 }.bind(this)); 66 67 // Try to render, what is already available. 68 this.renderList_(); 69 } 70 71 ActionChoice.prototype = { __proto__: cr.EventTarget.prototype }; 72 73 /** 74 * The number of previews shown. 75 * @type {number} 76 * @const 77 */ 78 ActionChoice.PREVIEW_COUNT = 3; 79 80 /** 81 * Extension id of Google+ Photos app. 82 * @type {string} 83 * @const 84 */ 85 ActionChoice.GPLUS_PHOTOS_EXTENSION_ID = 'efjnaogkjbogokcnohkmnjdojkikgobo'; 86 87 /** 88 * Loads app in the document body. 89 * @param {FileSystem=} opt_filesystem Local file system. 90 * @param {Object=} opt_params Parameters. 91 */ 92 ActionChoice.load = function(opt_filesystem, opt_params) { 93 ImageUtil.metrics = metrics; 94 95 var hash = location.hash ? decodeURIComponent(location.hash.substr(1)) : ''; 96 var query = 97 location.search ? decodeURIComponent(location.search.substr(1)) : ''; 98 var params = opt_params || {}; 99 if (!params.source) params.source = hash; 100 if (!params.advancedMode) params.advancedMode = (query == 'advanced-mode'); 101 if (!params.metadataCache) params.metadataCache = MetadataCache.createFull(); 102 103 var onFilesystem = function(filesystem) { 104 var dom = document.querySelector('.action-choice'); 105 ActionChoice.instance = new ActionChoice(dom, filesystem, params); 106 }; 107 108 chrome.fileBrowserPrivate.getStrings(function(strings) { 109 loadTimeData.data = strings; 110 i18nTemplate.process(document, loadTimeData); 111 if (opt_filesystem) { 112 onFilesystem(opt_filesystem); 113 } else { 114 chrome.fileBrowserPrivate.requestFileSystem(onFilesystem); 115 } 116 }); 117 }; 118 119 /** 120 * Registers an action. 121 * @param {Object} action Action item. 122 * @private 123 */ 124 ActionChoice.prototype.registerAction_ = function(action) { 125 this.actions_.push(action); 126 this.actionsById_[action.id] = action; 127 }; 128 129 /** 130 * Initializes the source and Drive. If the remembered choice is available, 131 * then performs the action. 132 * @private 133 */ 134 ActionChoice.prototype.initializeVolumes_ = function() { 135 var checkDriveFinished = false; 136 var loadSourceFinished = false; 137 138 var maybeRunRememberedAction = function() { 139 if (!checkDriveFinished || !loadSourceFinished) 140 return; 141 142 // Run the remembered action if it is available. 143 if (this.rememberedChoice_) { 144 var action = this.actionsById_[this.rememberedChoice_]; 145 if (action && !action.disabled) 146 this.runAction_(action); 147 } 148 }.bind(this); 149 150 var onCheckDriveFinished = function() { 151 checkDriveFinished = true; 152 maybeRunRememberedAction(); 153 }; 154 155 var onLoadSourceFinished = function() { 156 loadSourceFinished = true; 157 maybeRunRememberedAction(); 158 }; 159 160 this.checkDrive_(onCheckDriveFinished); 161 this.loadSource_(this.params_.source, onLoadSourceFinished); 162 }; 163 164 /** 165 * One-time initialization of dom elements. 166 * @private 167 */ 168 ActionChoice.prototype.initDom_ = function() { 169 this.list_ = new cr.ui.List(); 170 this.list_.id = 'actions-list'; 171 this.document_.querySelector('.choices').appendChild(this.list_); 172 173 var self = this; // .bind(this) doesn't work on constructors. 174 this.list_.itemConstructor = function(item) { 175 return self.renderItem(item); 176 }; 177 178 this.list_.selectionModel = new cr.ui.ListSingleSelectionModel(); 179 this.list_.dataModel = new cr.ui.ArrayDataModel([]); 180 this.list_.autoExpands = true; 181 182 var acceptActionBound = function() { 183 this.acceptAction_(); 184 }.bind(this); 185 this.list_.activateItemAtIndex = acceptActionBound; 186 this.list_.addEventListener('click', acceptActionBound); 187 188 this.previews_ = this.document_.querySelector('.previews'); 189 this.counter_ = this.document_.querySelector('.counter'); 190 this.document_.addEventListener('keydown', this.onKeyDown_.bind(this)); 191 192 metrics.startInterval('PhotoImport.Load'); 193 this.dom_.setAttribute('loading', ''); 194 }; 195 196 /** 197 * Renders the list. 198 * @private 199 */ 200 ActionChoice.prototype.renderList_ = function() { 201 var currentItem = this.list_.dataModel.item( 202 this.list_.selectionModel.selectedIndex); 203 204 this.list_.startBatchUpdates(); 205 this.list_.dataModel.splice(0, this.list_.dataModel.length); 206 207 for (var i = 0; i < this.actions_.length; i++) { 208 if (!this.actions_[i].hidden) 209 this.list_.dataModel.push(this.actions_[i]); 210 } 211 212 for (var i = 0; i < this.list_.dataModel.length; i++) { 213 if (this.list_.dataModel.item(i) == currentItem) { 214 this.list_.selectionModel.selectedIndex = i; 215 break; 216 } 217 } 218 219 this.list_.endBatchUpdates(); 220 }; 221 222 /** 223 * Renders an item in the list. 224 * @param {Object} item Item to render. 225 * @return {Element} DOM element with representing the item. 226 */ 227 ActionChoice.prototype.renderItem = function(item) { 228 var result = this.document_.createElement('li'); 229 230 var div = this.document_.createElement('div'); 231 if (item.disabled && item.disabledTitle) 232 div.textContent = item.disabledTitle; 233 else 234 div.textContent = item.title; 235 236 if (item.class) 237 div.classList.add(item.class); 238 if (item.icon100 && item.icon200) 239 div.style.backgroundImage = '-webkit-image-set(' + 240 'url(' + item.icon100 + ') 1x,' + 241 'url(' + item.icon200 + ') 2x)'; 242 if (item.disabled) 243 div.classList.add('disabled'); 244 245 cr.defineProperty(result, 'lead', cr.PropertyKind.BOOL_ATTR); 246 cr.defineProperty(result, 'selected', cr.PropertyKind.BOOL_ATTR); 247 result.appendChild(div); 248 249 return result; 250 }; 251 252 /** 253 * Checks whether Drive is reachable. 254 * 255 * @param {function()} callback Completion callback. 256 * @private 257 */ 258 ActionChoice.prototype.checkDrive_ = function(callback) { 259 var onMounted = function() { 260 this.importPhotosToDriveAction_.disabled = false; 261 this.renderList_(); 262 callback(); 263 }.bind(this); 264 265 if (this.volumeManager_.isMounted(RootDirectory.DRIVE)) { 266 onMounted(); 267 } else { 268 this.volumeManager_.mountDrive(onMounted, callback); 269 } 270 }; 271 272 /** 273 * Load the source contents. 274 * 275 * @param {string} source Path to source. 276 * @param {function()} callback Completion callback. 277 * @private 278 */ 279 ActionChoice.prototype.loadSource_ = function(source, callback) { 280 var onTraversed = function(results) { 281 metrics.recordInterval('PhotoImport.Scan'); 282 var videos = results.filter(FileType.isVideo); 283 if (videos.length == 1) { 284 this.singleVideo_ = videos[0]; 285 this.watchSingleVideoAction_.title = loadTimeData.getStringF( 286 'ACTION_CHOICE_WATCH_SINGLE_VIDEO', videos[0].name); 287 this.watchSingleVideoAction_.hidden = false; 288 this.watchSingleVideoAction_.disabled = false; 289 this.renderList_(); 290 } 291 292 var mediaFiles = results.filter(FileType.isImageOrVideo); 293 if (mediaFiles.length == 0) { 294 // If we have no media files, the only choice is view files. So, don't 295 // confuse user with a single choice, and just open file manager. 296 this.viewFiles_(); 297 this.recordAction_('view-files-auto'); 298 this.close_(); 299 } 300 301 if (mediaFiles.length < ActionChoice.PREVIEW_COUNT) { 302 this.counter_.textContent = loadTimeData.getStringF( 303 'ACTION_CHOICE_COUNTER_NO_MEDIA', results.length); 304 } else { 305 this.counter_.textContent = loadTimeData.getStringF( 306 'ACTION_CHOICE_COUNTER', mediaFiles.length); 307 } 308 var previews = mediaFiles.length ? mediaFiles : results; 309 var previewsCount = Math.min(ActionChoice.PREVIEW_COUNT, previews.length); 310 this.renderPreview_(previews, previewsCount); 311 callback(); 312 }.bind(this); 313 314 var onEntry = function(entry) { 315 this.sourceEntry_ = entry; 316 this.document_.querySelector('title').textContent = entry.name; 317 318 var deviceType = this.volumeManager_.getDeviceType(entry.fullPath); 319 if (deviceType != 'sd') deviceType = 'usb'; 320 this.dom_.querySelector('.device-type').setAttribute('device-type', 321 deviceType); 322 this.dom_.querySelector('.loading-text').textContent = 323 loadTimeData.getString('ACTION_CHOICE_LOADING_' + 324 deviceType.toUpperCase()); 325 326 util.traverseTree(entry, onTraversed, 0 /* infinite depth */, 327 FileType.isVisible); 328 }.bind(this); 329 330 var onReady = function() { 331 util.resolvePath(this.filesystem_.root, source, onEntry, function() { 332 this.recordAction_('error'); 333 this.close_(); 334 }.bind(this)); 335 }.bind(this); 336 337 this.sourceEntry_ = null; 338 metrics.startInterval('PhotoImport.Scan'); 339 if (!this.volumeManager_.isReady()) 340 this.volumeManager_.addEventListener('ready', onReady); 341 else 342 onReady(); 343 }; 344 345 /** 346 * Renders a preview for a media entry. 347 * @param {Array.<FileEntry>} entries The entries. 348 * @param {number} count Remaining count. 349 * @private 350 */ 351 ActionChoice.prototype.renderPreview_ = function(entries, count) { 352 var entry = entries.shift(); 353 var box = this.document_.createElement('div'); 354 box.className = 'img-container'; 355 356 var done = function() { 357 this.dom_.removeAttribute('loading'); 358 metrics.recordInterval('PhotoImport.Load'); 359 }.bind(this); 360 361 var onSuccess = function() { 362 this.previews_.appendChild(box); 363 if (--count == 0) { 364 done(); 365 } else { 366 this.renderPreview_(entries, count); 367 } 368 }.bind(this); 369 370 var onError = function() { 371 if (entries.length == 0) { 372 // Append one image with generic thumbnail. 373 this.previews_.appendChild(box); 374 done(); 375 } else { 376 this.renderPreview_(entries, count); 377 } 378 }.bind(this); 379 380 this.metadataCache_.get(entry, 'thumbnail|filesystem', 381 function(metadata) { 382 new ThumbnailLoader(entry.toURL(), 383 ThumbnailLoader.LoaderType.IMAGE, 384 metadata).load( 385 box, 386 ThumbnailLoader.FillMode.FILL, 387 ThumbnailLoader.OptimizationMode.NEVER_DISCARD, 388 onSuccess, 389 onError, 390 onError); 391 }); 392 }; 393 394 /** 395 * Closes the window. 396 * @private 397 */ 398 ActionChoice.prototype.close_ = function() { 399 window.close(); 400 }; 401 402 /** 403 * Keydown event handler. 404 * @param {Event} e The event. 405 * @private 406 */ 407 ActionChoice.prototype.onKeyDown_ = function(e) { 408 switch (util.getKeyModifiers(e) + e.keyCode) { 409 case '13': 410 this.acceptAction_(); 411 break; 412 case '27': 413 this.recordAction_('close'); 414 this.close_(); 415 break; 416 } 417 }; 418 419 /** 420 * Runs an action. 421 * @param {Object} action Action item to perform. 422 * @private 423 */ 424 ActionChoice.prototype.runAction_ = function(action) { 425 // TODO(mtomasz): Remove these predefined actions in Apps v2. 426 if (action == this.importPhotosToDriveAction_) { 427 var url = chrome.runtime.getURL('photo_import.html') + 428 '#' + this.sourceEntry_.fullPath; 429 var width = 728; 430 var height = 656; 431 var top = Math.round((window.screen.availHeight - height) / 2); 432 var left = Math.round((window.screen.availWidth - width) / 2); 433 chrome.app.window.create(url, 434 {height: height, width: width, left: left, top: top}); 435 this.recordAction_('import-photos-to-drive'); 436 this.close_(); 437 return; 438 } 439 440 if (action == this.watchSingleVideoAction_) { 441 chrome.fileBrowserPrivate.viewFiles([this.singleVideo_.toURL()], 442 function(success) {}); 443 this.recordAction_('watch-single-video'); 444 this.close_(); 445 return; 446 } 447 448 if (action == this.viewFilesAction_) { 449 this.viewFiles_(); 450 this.recordAction_('view-files'); 451 this.close_(); 452 return; 453 } 454 455 if (!action.extensionId) { 456 console.error('Unknown predefined action.'); 457 return; 458 } 459 460 // Run the media galleries handler. 461 chrome.mediaGalleriesPrivate.launchHandler(action.extensionId, 462 action.actionId, 463 this.params_.source); 464 this.close_(); 465 }; 466 467 /** 468 * Handles accepting an action. Checks if the action is available, remembers 469 * and runs it. 470 * @private 471 */ 472 ActionChoice.prototype.acceptAction_ = function() { 473 var action = 474 this.list_.dataModel.item(this.list_.selectionModel.selectedIndex); 475 if (!action || action.hidden || action.disabled) 476 return; 477 478 this.runAction_(action); 479 ActionChoiceUtil.setRememberedActionId(action.id); 480 }; 481 482 /** 483 * Called when some device is unmounted. 484 * @param {Event} event Event object. 485 * @private 486 */ 487 ActionChoice.prototype.onDeviceUnmounted_ = function(event) { 488 if (this.sourceEntry_ && event.mountPath == this.sourceEntry_.fullPath) 489 window.close(); 490 }; 491 492 /** 493 * Perform the 'view files' action. 494 * @private 495 */ 496 ActionChoice.prototype.viewFiles_ = function() { 497 var path = this.sourceEntry_.fullPath; 498 chrome.runtime.getBackgroundPage(function(bg) { 499 bg.launchFileManager({defaultPath: path}); 500 }); 501 }; 502 503 /** 504 * Records an action chosen. 505 * @param {string} action Action name. 506 * @private 507 */ 508 ActionChoice.prototype.recordAction_ = function(action) { 509 metrics.recordEnum('PhotoImport.Action', action, 510 ['import-photos-to-drive', 511 'view-files', 512 'view-files-auto', 513 'watch-single-video', 514 'error', 515 'close']); 516 }; 517