Home | History | Annotate | Download | only in js
      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