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 |