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