OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 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 PhotoImport.load(); | |
9 }); | |
10 | |
11 /** | |
12 * The main Photo App object. | |
13 * @param {HTMLElement} dom Container. | |
14 * @param {VolumeManagerWrapper} volumeManager The initialized | |
15 * VolumeManagerWrapper instance. | |
16 * @param {Object} params Parameters. | |
17 * @constructor | |
18 */ | |
19 function PhotoImport(dom, volumeManager, params) { | |
20 this.dom_ = dom; | |
21 this.document_ = this.dom_.ownerDocument; | |
22 this.metadataCache_ = params.metadataCache; | |
23 this.volumeManager_ = volumeManager; | |
24 this.fileOperationManager_ = FileOperationManagerWrapper.getInstance(); | |
25 this.mediaFilesList_ = null; | |
26 this.destination_ = null; | |
27 this.myPhotosDirectory_ = null; | |
28 this.parentWindowId_ = params.parentWindowId; | |
29 | |
30 this.initDom_(); | |
31 this.initMyPhotos_(); | |
32 this.loadSource_(params.source); | |
33 } | |
34 | |
35 PhotoImport.prototype = { __proto__: cr.EventTarget.prototype }; | |
36 | |
37 /** | |
38 * Single item width. | |
39 * Keep in sync with .grid-item rule in photo_import.css. | |
40 */ | |
41 PhotoImport.ITEM_WIDTH = 164 + 8; | |
42 | |
43 /** | |
44 * Number of tries in creating a destination directory. | |
45 */ | |
46 PhotoImport.CREATE_DESTINATION_TRIES = 100; | |
47 | |
48 /** | |
49 * Loads app in the document body. | |
50 * @param {Object=} opt_params Parameters. | |
51 */ | |
52 PhotoImport.load = function(opt_params) { | |
53 ImageUtil.metrics = metrics; | |
54 | |
55 var hash = location.hash ? location.hash.substr(1) : ''; | |
56 var query = location.search ? location.search.substr(1) : ''; | |
57 var params = opt_params || {}; | |
58 if (!params.source) params.source = hash; | |
59 if (!params.parentWindowId && query) params.parentWindowId = query; | |
60 if (!params.metadataCache) params.metadataCache = MetadataCache.createFull(); | |
61 | |
62 var api = chrome.fileBrowserPrivate || window.top.chrome.fileBrowserPrivate; | |
63 api.getStrings(function(strings) { | |
64 loadTimeData.data = strings; | |
65 var dom = document.querySelector('.photo-import'); | |
66 | |
67 var volumeManager = new VolumeManagerWrapper( | |
68 VolumeManagerWrapper.DriveEnabledStatus.DRIVE_ENABLED); | |
69 volumeManager.ensureInitialized(function() { | |
70 new PhotoImport(dom, volumeManager, params); | |
71 }); | |
72 }); | |
73 }; | |
74 | |
75 /** | |
76 * One-time initialization of dom elements. | |
77 * @private | |
78 */ | |
79 PhotoImport.prototype.initDom_ = function() { | |
80 this.dom_.setAttribute('loading', ''); | |
81 this.dom_.ownerDocument.defaultView.addEventListener( | |
82 'resize', this.onResize_.bind(this)); | |
83 | |
84 this.spinner_ = this.dom_.querySelector('.spinner'); | |
85 | |
86 this.document_.querySelector('title').textContent = | |
87 loadTimeData.getString('PHOTO_IMPORT_TITLE'); | |
88 this.dom_.querySelector('.caption').textContent = | |
89 loadTimeData.getString('PHOTO_IMPORT_CAPTION'); | |
90 this.selectAllNone_ = this.dom_.querySelector('.select'); | |
91 this.selectAllNone_.addEventListener('click', | |
92 this.onSelectAllNone_.bind(this)); | |
93 | |
94 this.dom_.querySelector('label[for=delete-after-checkbox]').textContent = | |
95 loadTimeData.getString('PHOTO_IMPORT_DELETE_AFTER'); | |
96 this.selectedCount_ = this.dom_.querySelector('.selected-count'); | |
97 | |
98 this.importButton_ = this.dom_.querySelector('button.import'); | |
99 this.importButton_.textContent = | |
100 loadTimeData.getString('PHOTO_IMPORT_IMPORT_BUTTON'); | |
101 this.importButton_.addEventListener('click', this.onImportClick_.bind(this)); | |
102 | |
103 this.cancelButton_ = this.dom_.querySelector('button.cancel'); | |
104 this.cancelButton_.textContent = str('CANCEL_LABEL'); | |
105 this.cancelButton_.addEventListener('click', this.onCancelClick_.bind(this)); | |
106 | |
107 this.grid_ = this.dom_.querySelector('grid'); | |
108 cr.ui.Grid.decorate(this.grid_); | |
109 this.grid_.itemConstructor = GridItem.bind(null, this); | |
110 this.fileList_ = new cr.ui.ArrayDataModel([]); | |
111 this.grid_.selectionModel = new cr.ui.ListSelectionModel(); | |
112 this.grid_.dataModel = this.fileList_; | |
113 this.grid_.selectionModel.addEventListener('change', | |
114 this.onSelectionChanged_.bind(this)); | |
115 this.onSelectionChanged_(); | |
116 | |
117 this.importingDialog_ = new ImportingDialog( | |
118 this.dom_, this.fileOperationManager_, | |
119 this.metadataCache_, this.parentWindowId_); | |
120 | |
121 var dialogs = cr.ui.dialogs; | |
122 dialogs.BaseDialog.OK_LABEL = str('OK_LABEL'); | |
123 dialogs.BaseDialog.CANCEL_LABEL = str('CANCEL_LABEL'); | |
124 this.alert_ = new dialogs.AlertDialog(this.dom_); | |
125 }; | |
126 | |
127 /** | |
128 * One-time initialization of the My Photos directory. | |
129 * @private | |
130 */ | |
131 PhotoImport.prototype.initMyPhotos_ = function() { | |
132 var driveVolume = this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE); | |
133 if (!driveVolume || driveVolume.error || !driveVolume.root) { | |
134 this.onError_(loadTimeData.getString('PHOTO_IMPORT_DRIVE_ERROR')); | |
135 return; | |
136 } | |
137 | |
138 util.getOrCreateDirectory( | |
139 driveVolume.root, | |
140 loadTimeData.getString('PHOTO_IMPORT_MY_PHOTOS_DIRECTORY_NAME'), | |
141 function(entry) { | |
142 // This may enable the import button, so check that. | |
143 this.myPhotosDirectory_ = entry; | |
144 this.onSelectionChanged_(); | |
145 }, | |
146 function(error) { | |
147 this.onError_(loadTimeData.getString('PHOTO_IMPORT_DRIVE_ERROR')); | |
148 }.bind(this)); | |
149 }; | |
150 | |
151 /** | |
152 * Creates the destination directory. | |
153 * @param {function} onSuccess Callback on success. | |
154 * @private | |
155 */ | |
156 PhotoImport.prototype.createDestination_ = function(onSuccess) { | |
157 var onError = this.onError_.bind( | |
158 this, loadTimeData.getString('PHOTO_IMPORT_DESTINATION_ERROR')); | |
159 | |
160 var dateFormatter = Intl.DateTimeFormat( | |
161 [] /* default locale */, | |
162 {year: 'numeric', month: 'short', day: 'numeric'}); | |
163 | |
164 var baseName = PathUtil.join( | |
165 RootDirectory.DRIVE, | |
166 loadTimeData.getString('PHOTO_IMPORT_MY_PHOTOS_DIRECTORY_NAME'), | |
167 dateFormatter.format(new Date())); | |
168 | |
169 var driveVolume = this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE); | |
170 if (!driveVolume || driveVolume.error || !driveVolume.root) { | |
171 onError(); | |
172 return; | |
173 } | |
174 | |
175 var tryNext = function(number) { | |
176 if (number > PhotoImport.CREATE_DESTINATION_TRIES) { | |
177 console.error('Too many directories with the same base name exist.'); | |
178 onError(); | |
179 return; | |
180 } | |
181 | |
182 var directoryName = baseName; | |
183 if (number > 1) | |
184 directoryName += ' (' + (tryNumber) + ')'; | |
185 driveVolume.root.getDirectory( | |
186 directoryName, | |
187 {create: true, exclusive: true}, | |
188 function(entry) { | |
189 this.destination_ = entry; | |
190 onSuccess(); | |
191 }.bind(this), | |
192 function(error) { | |
193 if (error.code === FileError.PATH_EXISTS_ERR) { | |
194 // If there already exists an entry, retry with incrementing the | |
195 // number. | |
196 tryNext(number + 1); | |
197 return; | |
198 } | |
199 onError(); | |
200 }.bind(this)); | |
201 }.bind(this); | |
202 | |
203 tryNext(1); | |
204 }; | |
205 | |
206 | |
207 /** | |
208 * Load the source contents. | |
209 * @param {string} source Path to source. | |
210 * @private | |
211 */ | |
212 PhotoImport.prototype.loadSource_ = function(source) { | |
213 var onError = this.onError_.bind( | |
214 this, loadTimeData.getString('PHOTO_IMPORT_SOURCE_ERROR')); | |
215 | |
216 var result = []; | |
217 this.volumeManager_.resolvePath( | |
218 source, | |
219 function(sourceEntry) { | |
220 util.traverseTree( | |
221 entry, | |
222 function(entry) { | |
223 if (!FileType.isVisible(entry)) | |
224 return false; | |
225 if (FileType.isImageOrVideo(entry)) | |
226 result.push(entry); | |
227 return true; | |
228 }, | |
229 function() { | |
230 this.dom_.removeAttribute('loading'); | |
231 this.mediaFilesList_ = result; | |
232 this.fillGrid_(); | |
233 }.bind(this), | |
234 onError); | |
235 }.bind(this), | |
236 onError); | |
237 }; | |
238 | |
239 /** | |
240 * Renders files into grid. | |
241 * @private | |
242 */ | |
243 PhotoImport.prototype.fillGrid_ = function() { | |
244 if (!this.mediaFilesList_) return; | |
245 this.fileList_.splice(0, this.fileList_.length); | |
246 this.fileList_.push.apply(this.fileList_, this.mediaFilesList_); | |
247 }; | |
248 | |
249 /** | |
250 * Creates groups for files based on modification date. | |
251 * @param {Array.<Entry>} files File list. | |
252 * @param {Object} filesystem Filesystem metadata. | |
253 * @return {Array.<Object>} List of grouped items. | |
254 * @private | |
255 */ | |
256 PhotoImport.prototype.createGroups_ = function(files, filesystem) { | |
257 var dateFormatter = Intl.DateTimeFormat( | |
258 [] /* default locale */, | |
259 {year: 'numeric', month: 'short', day: 'numeric'}); | |
260 | |
261 var columns = this.grid_.columns; | |
262 | |
263 var unknownGroup = { | |
264 type: 'group', | |
265 date: 0, | |
266 title: loadTimeData.getString('PHOTO_IMPORT_UNKNOWN_DATE'), | |
267 items: [] | |
268 }; | |
269 | |
270 var groupsMap = {}; | |
271 | |
272 for (var index = 0; index < files.length; index++) { | |
273 var props = filesystem[index]; | |
274 var item = { type: 'entry', entry: files[index] }; | |
275 | |
276 if (!props || !props.modificationTime) { | |
277 item.group = unknownGroup; | |
278 unknownGroup.items.push(item); | |
279 continue; | |
280 } | |
281 | |
282 var date = new Date(props.modificationTime); | |
283 date.setHours(0); | |
284 date.setMinutes(0); | |
285 date.setSeconds(0); | |
286 date.setMilliseconds(0); | |
287 | |
288 var time = date.getTime(); | |
289 if (!(time in groupsMap)) { | |
290 groupsMap[time] = { | |
291 type: 'group', | |
292 date: date, | |
293 title: dateFormatter.format(date), | |
294 items: [] | |
295 }; | |
296 } | |
297 | |
298 var group = groupsMap[time]; | |
299 group.items.push(item); | |
300 item.group = group; | |
301 } | |
302 | |
303 var groups = []; | |
304 for (var time in groupsMap) { | |
305 if (groupsMap.hasOwnProperty(time)) { | |
306 groups.push(groupsMap[time]); | |
307 } | |
308 } | |
309 if (unknownGroup.items.length > 0) | |
310 groups.push(unknownGroup); | |
311 | |
312 groups.sort(function(a, b) { | |
313 return b.date.getTime() - a.date.getTime(); | |
314 }); | |
315 | |
316 var list = []; | |
317 for (var index = 0; index < groups.length; index++) { | |
318 var group = groups[index]; | |
319 | |
320 list.push(group); | |
321 for (var t = 1; t < columns; t++) { | |
322 list.push({ type: 'empty' }); | |
323 } | |
324 | |
325 for (var j = 0; j < group.items.length; j++) { | |
326 list.push(group.items[j]); | |
327 } | |
328 | |
329 var count = group.items.length; | |
330 while (count % columns != 0) { | |
331 list.push({ type: 'empty' }); | |
332 count++; | |
333 } | |
334 } | |
335 | |
336 return list; | |
337 }; | |
338 | |
339 /** | |
340 * Decorates grid item. | |
341 * @param {HTMLLIElement} li The list item. | |
342 * @param {FileEntry} entry The file entry. | |
343 * @private | |
344 */ | |
345 PhotoImport.prototype.decorateGridItem_ = function(li, entry) { | |
346 li.className = 'grid-item'; | |
347 li.entry = entry; | |
348 | |
349 var frame = this.document_.createElement('div'); | |
350 frame.className = 'grid-frame'; | |
351 li.appendChild(frame); | |
352 | |
353 var box = this.document_.createElement('div'); | |
354 box.className = 'img-container'; | |
355 this.metadataCache_.get(entry, 'thumbnail|filesystem', | |
356 function(metadata) { | |
357 new ThumbnailLoader(entry.toURL(), | |
358 ThumbnailLoader.LoaderType.IMAGE, | |
359 metadata). | |
360 load(box, ThumbnailLoader.FillMode.FIT, | |
361 ThumbnailLoader.OptimizationMode.DISCARD_DETACHED); | |
362 }); | |
363 frame.appendChild(box); | |
364 | |
365 var check = this.document_.createElement('div'); | |
366 check.className = 'check'; | |
367 li.appendChild(check); | |
368 }; | |
369 | |
370 /** | |
371 * Handles the 'pick all/none' action. | |
372 * @private | |
373 */ | |
374 PhotoImport.prototype.onSelectAllNone_ = function() { | |
375 var sm = this.grid_.selectionModel; | |
376 if (sm.selectedIndexes.length == this.fileList_.length) { | |
377 sm.unselectAll(); | |
378 } else { | |
379 sm.selectAll(); | |
380 } | |
381 }; | |
382 | |
383 /** | |
384 * Show error message. | |
385 * @param {string} message Error message. | |
386 * @private | |
387 */ | |
388 PhotoImport.prototype.onError_ = function(message) { | |
389 this.importingDialog_.hide(function() { | |
390 this.alert_.show(message, | |
391 function() { | |
392 window.close(); | |
393 }); | |
394 }.bind(this)); | |
395 }; | |
396 | |
397 /** | |
398 * Resize event handler. | |
399 * @private | |
400 */ | |
401 PhotoImport.prototype.onResize_ = function() { | |
402 var g = this.grid_; | |
403 g.startBatchUpdates(); | |
404 setTimeout(function() { | |
405 g.columns = 0; | |
406 g.redraw(); | |
407 g.endBatchUpdates(); | |
408 }, 0); | |
409 }; | |
410 | |
411 /** | |
412 * @return {Array.<Object>} The list of selected entries. | |
413 * @private | |
414 */ | |
415 PhotoImport.prototype.getSelectedItems_ = function() { | |
416 var indexes = this.grid_.selectionModel.selectedIndexes; | |
417 var list = []; | |
418 for (var i = 0; i < indexes.length; i++) { | |
419 list.push(this.fileList_.item(indexes[i])); | |
420 } | |
421 return list; | |
422 }; | |
423 | |
424 /** | |
425 * Event handler for picked items change. | |
426 * @private | |
427 */ | |
428 PhotoImport.prototype.onSelectionChanged_ = function() { | |
429 var count = this.grid_.selectionModel.selectedIndexes.length; | |
430 this.selectedCount_.textContent = count == 0 ? '' : | |
431 count == 1 ? loadTimeData.getString('PHOTO_IMPORT_ONE_SELECTED') : | |
432 loadTimeData.getStringF('PHOTO_IMPORT_MANY_SELECTED', count); | |
433 this.importButton_.disabled = count == 0 || this.myPhotosDirectory_ == null; | |
434 this.selectAllNone_.textContent = loadTimeData.getString( | |
435 count == this.fileList_.length && count > 0 ? | |
436 'PHOTO_IMPORT_SELECT_NONE' : 'PHOTO_IMPORT_SELECT_ALL'); | |
437 }; | |
438 | |
439 /** | |
440 * Event handler for import button click. | |
441 * @param {Event} event The event. | |
442 * @private | |
443 */ | |
444 PhotoImport.prototype.onImportClick_ = function(event) { | |
445 var entries = this.getSelectedItems_(); | |
446 var move = this.dom_.querySelector('#delete-after-checkbox').checked; | |
447 this.importingDialog_.show(entries, move); | |
448 | |
449 this.createDestination_(function() { | |
450 var percentage = Math.round(entries.length / this.fileList_.length * 100); | |
451 metrics.recordMediumCount('PhotoImport.ImportCount', entries.length); | |
452 metrics.recordSmallCount('PhotoImport.ImportPercentage', percentage); | |
453 | |
454 this.importingDialog_.start(this.destination_); | |
455 }.bind(this)); | |
456 }; | |
457 | |
458 /** | |
459 * Click event handler for the cancel button. | |
460 * @param {Event} event The event. | |
461 * @private | |
462 */ | |
463 PhotoImport.prototype.onCancelClick_ = function(event) { | |
464 window.close(); | |
465 }; | |
466 | |
467 /** | |
468 * Item in the grid. | |
469 * @param {PhotoImport} app Application instance. | |
470 * @param {Entry} entry File entry. | |
471 * @constructor | |
472 */ | |
473 function GridItem(app, entry) { | |
474 var li = app.document_.createElement('li'); | |
475 li.__proto__ = GridItem.prototype; | |
476 app.decorateGridItem_(li, entry); | |
477 return li; | |
478 } | |
479 | |
480 GridItem.prototype = { | |
481 __proto__: cr.ui.ListItem.prototype, | |
482 get label() {}, | |
483 set label(value) {} | |
484 }; | |
485 | |
486 /** | |
487 * Creates a selection controller that is to be used with grid. | |
488 * @param {cr.ui.ListSelectionModel} selectionModel The selection model to | |
489 * interact with. | |
490 * @param {cr.ui.Grid} grid The grid to interact with. | |
491 * @constructor | |
492 * @extends {!cr.ui.ListSelectionController} | |
493 */ | |
494 function GridSelectionController(selectionModel, grid) { | |
495 this.selectionModel_ = selectionModel; | |
496 this.grid_ = grid; | |
497 } | |
498 | |
499 /** | |
500 * Extends cr.ui.ListSelectionController. | |
501 */ | |
502 GridSelectionController.prototype.__proto__ = | |
503 cr.ui.ListSelectionController.prototype; | |
504 | |
505 /** @override */ | |
506 GridSelectionController.prototype.getIndexBelow = function(index) { | |
507 if (index == this.getLastIndex()) { | |
508 return -1; | |
509 } | |
510 | |
511 var dm = this.grid_.dataModel; | |
512 var columns = this.grid_.columns; | |
513 var min = (Math.floor(index / columns) + 1) * columns; | |
514 | |
515 for (var row = 1; true; row++) { | |
516 var end = index + columns * row; | |
517 var start = Math.max(min, index + columns * (row - 1)); | |
518 if (start > dm.length) break; | |
519 | |
520 for (var i = end; i > start; i--) { | |
521 if (i < dm.length && dm.item(i).type == 'entry') | |
522 return i; | |
523 } | |
524 } | |
525 | |
526 return this.getLastIndex(); | |
527 }; | |
528 | |
529 /** @override */ | |
530 GridSelectionController.prototype.getIndexAbove = function(index) { | |
531 if (index == this.getFirstIndex()) { | |
532 return -1; | |
533 } | |
534 | |
535 var dm = this.grid_.dataModel; | |
536 index -= this.grid_.columns; | |
537 while (index >= 0 && dm.item(index).type != 'entry') { | |
538 index--; | |
539 } | |
540 | |
541 return index < 0 ? this.getFirstIndex() : index; | |
542 }; | |
543 | |
544 /** @override */ | |
545 GridSelectionController.prototype.getIndexBefore = function(index) { | |
546 var dm = this.grid_.dataModel; | |
547 index--; | |
548 while (index >= 0 && dm.item(index).type != 'entry') { | |
549 index--; | |
550 } | |
551 return index; | |
552 }; | |
553 | |
554 /** @override */ | |
555 GridSelectionController.prototype.getIndexAfter = function(index) { | |
556 var dm = this.grid_.dataModel; | |
557 index++; | |
558 while (index < dm.length && dm.item(index).type != 'entry') { | |
559 index++; | |
560 } | |
561 return index == dm.length ? -1 : index; | |
562 }; | |
563 | |
564 /** @override */ | |
565 GridSelectionController.prototype.getFirstIndex = function() { | |
566 var dm = this.grid_.dataModel; | |
567 for (var index = 0; index < dm.length; index++) { | |
568 if (dm.item(index).type == 'entry') | |
569 return index; | |
570 } | |
571 return -1; | |
572 }; | |
573 | |
574 /** @override */ | |
575 GridSelectionController.prototype.getLastIndex = function() { | |
576 var dm = this.grid_.dataModel; | |
577 for (var index = dm.length - 1; index >= 0; index--) { | |
578 if (dm.item(index).type == 'entry') | |
579 return index; | |
580 } | |
581 return -1; | |
582 }; | |
OLD | NEW |