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 /** | |
6 * Base interface Ribbon uses to display photos. | |
7 * @interface | |
8 */ | |
9 function RibbonClient() {} | |
10 | |
11 /** | |
12 * Request prefetch for an image. | |
13 * @param {number} id The content id for caching. | |
14 * @param {string} url Image url. | |
15 */ | |
16 RibbonClient.prototype.prefetchImage = function(id, url) {}; | |
17 | |
18 //TODO(JSDOC) | |
19 RibbonClient.prototype.openImage = function(id, url, metadata, direction) {}; | |
20 | |
21 //TODO(JSDOC) | |
22 RibbonClient.prototype.closeImage = function(item) {}; | |
23 | |
24 /** | |
25 * Image gallery for viewing and editing image files. | |
26 * | |
27 * @param {HTMLDivElement} container Container to create gallery in. | |
28 * @param {Object} context Object containing the following: | |
29 * @class | |
30 * @constructor | |
31 * @implements {RibbonClient} | |
32 */ | |
33 function Gallery(container, context) { | |
34 this.container_ = container; | |
35 this.document_ = container.ownerDocument; | |
36 this.context_ = context; | |
37 this.metadataCache_ = context.metadataCache; | |
38 | |
39 var strf = context.displayStringFunction; | |
40 this.displayStringFunction_ = function(id, formatArgs) { | |
41 var args = Array.prototype.slice.call(arguments); | |
42 args[0] = 'GALLERY_' + id.toUpperCase(); | |
43 return strf.apply(null, args); | |
44 }; | |
45 | |
46 this.onFadeTimeoutBound_ = this.onFadeTimeout_.bind(this); | |
47 this.fadeTimeoutId_ = null; | |
48 this.mouseOverTool_ = false; | |
49 | |
50 this.initDom_(); | |
51 } | |
52 | |
53 Gallery.prototype = { __proto__: RibbonClient.prototype }; | |
54 | |
55 //TODO(JSDOC) | |
56 Gallery.open = function(context, items, selectedItem) { | |
57 var container = document.querySelector('.gallery'); | |
58 ImageUtil.removeChildren(container); | |
59 var gallery = new Gallery(container, context); | |
60 gallery.load(items, selectedItem); | |
61 }; | |
62 | |
63 /** | |
64 * List of available editor modes. | |
65 * @type {Array.<ImageEditor.Mode>} | |
66 */ | |
67 Gallery.editorModes = [ | |
68 new ImageEditor.Mode.InstantAutofix(), | |
69 new ImageEditor.Mode.Crop(), | |
70 new ImageEditor.Mode.Exposure(), | |
71 new ImageEditor.Mode.OneClick('rotate_left', new Command.Rotate(-1)), | |
72 new ImageEditor.Mode.OneClick('rotate_right', new Command.Rotate(1)) | |
73 ]; | |
74 | |
75 /** | |
76 * Fade timeout im milliseconds on image transition. | |
77 * @type {Number} | |
78 */ | |
79 Gallery.FADE_TIMEOUT = 3000; | |
80 | |
81 /** | |
82 * Fade timeout im milliseconds on first gallery load. | |
83 * @type {Number} | |
84 */ | |
85 Gallery.FIRST_FADE_TIMEOUT = 1000; | |
86 | |
87 //TODO(JSDOC) | |
88 Gallery.OVERWRITE_BUBBLE_MAX_TIMES = 5; | |
89 | |
90 /** | |
91 * Types of metadata Gallery uses (to query the metadata cache). | |
92 */ | |
93 Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|streaming'; | |
94 | |
95 /** | |
96 * Initializes gallery UI | |
97 * @private | |
98 */ | |
99 Gallery.prototype.initDom_ = function() { | |
100 var doc = this.document_; | |
101 | |
102 doc.oncontextmenu = function(e) { e.preventDefault(); }; | |
103 | |
104 doc.body.addEventListener('keydown', this.onKeyDown_.bind(this)); | |
105 | |
106 doc.body.addEventListener('mousemove', this.onMouseMove_.bind(this)); | |
107 | |
108 // Show tools when the user touches the screen. | |
109 doc.body.addEventListener('touchstart', this.cancelFading_.bind(this)); | |
110 var initiateFading = this.initiateFading_.bind(this, Gallery.FADE_TIMEOUT); | |
111 doc.body.addEventListener('touchend', initiateFading); | |
112 doc.body.addEventListener('touchcancel', initiateFading); | |
113 | |
114 window.addEventListener('unload', this.onUnload_.bind(this)); | |
115 | |
116 // We need to listen to the top window 'unload' and 'beforeunload' because | |
117 // the Gallery iframe does not get notified if the tab is closed. | |
118 this.onTopUnloadBound_ = this.onTopUnload_.bind(this); | |
119 window.top.addEventListener('unload', this.onTopUnloadBound_); | |
120 | |
121 this.onBeforeUnloadBound_ = this.onBeforeUnload_.bind(this); | |
122 window.top.addEventListener('beforeunload', this.onBeforeUnloadBound_); | |
123 | |
124 this.closeButton_ = doc.createElement('div'); | |
125 this.closeButton_.className = 'close tool dimmable'; | |
126 this.closeButton_.appendChild(doc.createElement('div')); | |
127 this.closeButton_.addEventListener('click', this.onClose_.bind(this)); | |
128 this.container_.appendChild(this.closeButton_); | |
129 | |
130 this.imageContainer_ = doc.createElement('div'); | |
131 this.imageContainer_.className = 'image-container'; | |
132 this.imageContainer_.addEventListener('click', (function() { | |
133 this.filenameEdit_.blur(); | |
134 if (this.isShowingVideo_()) | |
135 this.mediaControls_.togglePlayStateWithFeedback(); | |
136 }).bind(this)); | |
137 this.container_.appendChild(this.imageContainer_); | |
138 | |
139 this.toolbar_ = doc.createElement('div'); | |
140 this.toolbar_.className = 'toolbar tool dimmable'; | |
141 this.container_.appendChild(this.toolbar_); | |
142 | |
143 this.filenameSpacer_ = doc.createElement('div'); | |
144 this.filenameSpacer_.className = 'filename-spacer'; | |
145 this.toolbar_.appendChild(this.filenameSpacer_); | |
146 | |
147 this.bubble_ = doc.createElement('div'); | |
148 this.bubble_.className = 'bubble'; | |
149 var bubbleContent = doc.createElement('div'); | |
150 bubbleContent.innerHTML = this.displayStringFunction_('overwrite_bubble'); | |
151 this.bubble_.appendChild(bubbleContent); | |
152 var bubblePointer = doc.createElement('span'); | |
153 bubblePointer.className = 'pointer bottom'; | |
154 this.bubble_.appendChild(bubblePointer); | |
155 var bubbleClose = doc.createElement('div'); | |
156 bubbleClose.className = 'close-x'; | |
157 bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this)); | |
158 this.bubble_.appendChild(bubbleClose); | |
159 this.bubble_.hidden = true; | |
160 this.toolbar_.appendChild(this.bubble_); | |
161 | |
162 var nameBox = doc.createElement('div'); | |
163 nameBox.className = 'namebox'; | |
164 this.filenameSpacer_.appendChild(nameBox); | |
165 | |
166 this.filenameText_ = doc.createElement('div'); | |
167 this.filenameText_.addEventListener('click', | |
168 this.onFilenameClick_.bind(this)); | |
169 nameBox.appendChild(this.filenameText_); | |
170 | |
171 this.filenameEdit_ = doc.createElement('input'); | |
172 this.filenameEdit_.setAttribute('type', 'text'); | |
173 this.filenameEdit_.addEventListener('blur', | |
174 this.onFilenameEditBlur_.bind(this)); | |
175 this.filenameEdit_.addEventListener('keydown', | |
176 this.onFilenameEditKeydown_.bind(this)); | |
177 nameBox.appendChild(this.filenameEdit_); | |
178 | |
179 var options = doc.createElement('div'); | |
180 options.className = 'options'; | |
181 this.filenameSpacer_.appendChild(options); | |
182 | |
183 this.savedLabel_ = doc.createElement('div'); | |
184 this.savedLabel_.className = 'saved'; | |
185 this.savedLabel_.textContent = this.displayStringFunction_('saved'); | |
186 options.appendChild(this.savedLabel_); | |
187 | |
188 var overwriteOriginalBox = doc.createElement('div'); | |
189 overwriteOriginalBox.className = 'overwrite-original'; | |
190 options.appendChild(overwriteOriginalBox); | |
191 | |
192 this.overwriteOriginal_ = doc.createElement('input'); | |
193 this.overwriteOriginal_.type = 'checkbox'; | |
194 this.overwriteOriginal_.id = 'overwrite-checkbox'; | |
195 this.overwriteOriginal_.className = 'common white'; | |
196 overwriteOriginalBox.appendChild(this.overwriteOriginal_); | |
197 this.overwriteOriginal_.addEventListener('click', | |
198 this.onOverwriteOriginalClick_.bind(this)); | |
199 | |
200 var overwriteLabel = doc.createElement('label'); | |
201 overwriteLabel.textContent = | |
202 this.displayStringFunction_('overwrite_original'); | |
203 overwriteLabel.setAttribute('for', 'overwrite-checkbox'); | |
204 overwriteOriginalBox.appendChild(overwriteLabel); | |
205 | |
206 this.buttonSpacer_ = doc.createElement('div'); | |
207 this.buttonSpacer_.className = 'button-spacer'; | |
208 this.toolbar_.appendChild(this.buttonSpacer_); | |
209 | |
210 this.ribbonSpacer_ = doc.createElement('div'); | |
211 this.ribbonSpacer_.className = 'ribbon-spacer'; | |
212 this.toolbar_.appendChild(this.ribbonSpacer_); | |
213 | |
214 this.mediaSpacer_ = doc.createElement('div'); | |
215 this.mediaSpacer_.className = 'video-controls-spacer'; | |
216 this.container_.appendChild(this.mediaSpacer_); | |
217 | |
218 this.mediaToolbar_ = doc.createElement('div'); | |
219 this.mediaToolbar_.className = 'tool'; | |
220 this.mediaSpacer_.appendChild(this.mediaToolbar_); | |
221 | |
222 this.mediaControls_ = new VideoControls( | |
223 this.mediaToolbar_, | |
224 this.showErrorBanner_.bind(this, 'VIDEO_ERROR'), | |
225 this.toggleFullscreen_.bind(this), | |
226 this.container_); | |
227 | |
228 this.arrowBox_ = this.document_.createElement('div'); | |
229 this.arrowBox_.className = 'arrow-box'; | |
230 this.container_.appendChild(this.arrowBox_); | |
231 | |
232 this.arrowLeft_ = this.document_.createElement('div'); | |
233 this.arrowLeft_.className = 'arrow left tool dimmable'; | |
234 this.arrowLeft_.appendChild(doc.createElement('div')); | |
235 this.arrowBox_.appendChild(this.arrowLeft_); | |
236 | |
237 this.arrowSpacer_ = this.document_.createElement('div'); | |
238 this.arrowSpacer_.className = 'arrow-spacer'; | |
239 this.arrowBox_.appendChild(this.arrowSpacer_); | |
240 | |
241 this.arrowRight_ = this.document_.createElement('div'); | |
242 this.arrowRight_.className = 'arrow right tool dimmable'; | |
243 this.arrowRight_.appendChild(doc.createElement('div')); | |
244 this.arrowBox_.appendChild(this.arrowRight_); | |
245 | |
246 this.spinner_ = this.document_.createElement('div'); | |
247 this.spinner_.className = 'spinner'; | |
248 this.container_.appendChild(this.spinner_); | |
249 | |
250 this.errorWrapper_ = this.document_.createElement('div'); | |
251 this.errorWrapper_.className = 'prompt-wrapper'; | |
252 this.errorWrapper_.setAttribute('pos', 'center'); | |
253 this.container_.appendChild(this.errorWrapper_); | |
254 | |
255 this.errorBanner_ = this.document_.createElement('div'); | |
256 this.errorBanner_.className = 'error-banner'; | |
257 this.errorWrapper_.appendChild(this.errorBanner_); | |
258 | |
259 this.ribbon_ = new Ribbon(this.document_, | |
260 this, this.metadataCache_, this.arrowLeft_, this.arrowRight_); | |
261 this.ribbonSpacer_.appendChild(this.ribbon_); | |
262 | |
263 this.editBar_ = doc.createElement('div'); | |
264 this.editBar_.className = 'edit-bar'; | |
265 this.toolbar_.appendChild(this.editBar_); | |
266 | |
267 this.editButton_ = doc.createElement('div'); | |
268 this.editButton_.className = 'button edit'; | |
269 this.editButton_.textContent = this.displayStringFunction_('edit'); | |
270 this.editButton_.addEventListener('click', this.onEdit_.bind(this)); | |
271 this.toolbar_.appendChild(this.editButton_); | |
272 | |
273 this.editBarMain_ = doc.createElement('div'); | |
274 this.editBarMain_.className = 'edit-main'; | |
275 this.editBar_.appendChild(this.editBarMain_); | |
276 | |
277 this.editBarMode_ = doc.createElement('div'); | |
278 this.editBarMode_.className = 'edit-modal'; | |
279 this.container_.appendChild(this.editBarMode_); | |
280 | |
281 this.editBarModeWrapper_ = doc.createElement('div'); | |
282 this.editBarModeWrapper_.hidden = true; | |
283 this.editBarModeWrapper_.className = 'edit-modal-wrapper'; | |
284 this.editBarMode_.appendChild(this.editBarModeWrapper_); | |
285 | |
286 this.viewport_ = new Viewport(); | |
287 | |
288 this.imageView_ = new ImageView( | |
289 this.imageContainer_, | |
290 this.viewport_, | |
291 this.metadataCache_); | |
292 | |
293 this.editor_ = new ImageEditor( | |
294 this.viewport_, | |
295 this.imageView_, | |
296 { | |
297 root: this.container_, | |
298 image: this.imageContainer_, | |
299 toolbar: this.editBarMain_, | |
300 mode: this.editBarModeWrapper_ | |
301 }, | |
302 Gallery.editorModes, | |
303 this.displayStringFunction_); | |
304 | |
305 this.editor_.getBuffer().addOverlay(new SwipeOverlay(this.ribbon_)); | |
306 | |
307 this.imageView_.addContentCallback(this.onImageContentChanged_.bind(this)); | |
308 | |
309 this.editor_.trackWindow(doc.defaultView); | |
310 | |
311 Gallery.getFileBrowserPrivate().isFullscreen(function(fullscreen) { | |
312 this.originalFullscreen_ = fullscreen; | |
313 }.bind(this)); | |
314 }; | |
315 | |
316 /** | |
317 * BeforeUnload event listener. | |
318 * @param {Event} event Not used | |
319 * @return {string} Message to show if there's unsaved changes. | |
320 * @private | |
321 */ | |
322 Gallery.prototype.onBeforeUnload_ = function(event) { | |
323 if (this.editor_.isBusy()) | |
324 return this.displayStringFunction_('unsaved_changes'); | |
325 return null; | |
326 }; | |
327 | |
328 //TODO(JSDOC) | |
329 Gallery.prototype.load = function(items, selectedItem) { | |
330 var urls = []; | |
331 var selectedIndex = -1; | |
332 | |
333 // Convert canvas and blob items to blob urls. | |
334 for (var i = 0; i != items.length; i++) { | |
335 var item = items[i]; | |
336 var selected = (item == selectedItem); | |
337 | |
338 if (typeof item == 'string') { | |
339 if (selected) selectedIndex = urls.length; | |
340 urls.push(item); | |
341 } else { | |
342 console.error('Unsupported item type', item); | |
343 } | |
344 } | |
345 | |
346 if (urls.length == 0) | |
347 throw new Error('Cannot open the gallery for 0 items'); | |
348 | |
349 if (selectedIndex == -1) | |
350 throw new Error('Cannot find selected item'); | |
351 | |
352 var self = this; | |
353 | |
354 var selectedURL = urls[selectedIndex]; | |
355 | |
356 function initRibbon() { | |
357 self.ribbon_.load(urls, selectedIndex); | |
358 if (urls.length == 1 && self.isShowingVideo_()) { | |
359 // We only have one item and it is a video. Move the playback controls | |
360 // in place of thumbnails and start the playback immediately. | |
361 self.mediaSpacer_.removeChild(self.mediaToolbar_); | |
362 self.ribbonSpacer_.appendChild(self.mediaToolbar_); | |
363 self.mediaControls_.play(); | |
364 } | |
365 // Flash the toolbar briefly to let the user know it is there. | |
366 self.cancelFading_(); | |
367 self.initiateFading_(Gallery.FIRST_FADE_TIMEOUT); | |
368 } | |
369 | |
370 // Show the selected item ASAP, then complete the initialization (populating | |
371 // the ribbon can take especially long time). | |
372 this.metadataCache_.get(selectedURL, Gallery.METADATA_TYPE, | |
373 function(metadata) { | |
374 self.openImage(selectedIndex, selectedURL, metadata, 0, initRibbon); | |
375 }); | |
376 | |
377 this.context_.getShareActions(urls, function(tasks) { | |
378 if (tasks.length > 0) { | |
379 this.shareMode_ = new ShareMode(this.editor_, this.container_, | |
380 this.toolbar_, tasks, | |
381 this.onShare_.bind(this), this.onActionExecute_.bind(this), | |
382 this.displayStringFunction_); | |
383 } else { | |
384 this.shareMode_ = null; | |
385 } | |
386 }.bind(this)); | |
387 }; | |
388 | |
389 //TODO(JSDOC) | |
390 Gallery.prototype.onImageContentChanged_ = function() { | |
391 var revision = this.imageView_.getContentRevision(); | |
392 if (revision == 0) { | |
393 // Just loaded. | |
394 var key = 'gallery-overwrite-bubble'; | |
395 var times = key in localStorage ? parseInt(localStorage[key], 10) : 0; | |
396 if (times < Gallery.OVERWRITE_BUBBLE_MAX_TIMES) { | |
397 this.bubble_.hidden = false; | |
398 if (this.isEditing_()) { | |
399 localStorage[key] = times + 1; | |
400 } | |
401 } | |
402 } | |
403 | |
404 if (revision == 1) { | |
405 // First edit. | |
406 ImageUtil.setAttribute(this.filenameSpacer_, 'saved', true); | |
407 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit')); | |
408 } | |
409 }; | |
410 | |
411 //TODO(JSDOC) | |
412 Gallery.prototype.flashSavedLabel_ = function() { | |
413 var selLabelHighlighted = | |
414 ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted'); | |
415 setTimeout(selLabelHighlighted.bind(null, true), 0); | |
416 setTimeout(selLabelHighlighted.bind(null, false), 300); | |
417 }; | |
418 | |
419 //TODO(JSDOC) | |
420 Gallery.prototype.applyDefaultOverwrite_ = function() { | |
421 var key = 'gallery-overwrite-original'; | |
422 var overwrite = key in localStorage ? (localStorage[key] == 'true') : true; | |
423 this.overwriteOriginal_.checked = overwrite; | |
424 this.applyOverwrite_(overwrite); | |
425 }; | |
426 | |
427 //TODO(JSDOC) | |
428 Gallery.prototype.applyOverwrite_ = function(overwrite) { | |
429 if (overwrite) { | |
430 this.ribbon_.getSelectedItem().setOriginalName(this.context_.saveDirEntry, | |
431 this.updateFilename_.bind(this)); | |
432 } else { | |
433 this.ribbon_.getSelectedItem().setCopyName(this.context_.saveDirEntry, | |
434 this.selectedImageMetadata_, | |
435 this.updateFilename_.bind(this)); | |
436 } | |
437 }; | |
438 | |
439 //TODO(JSDOC) | |
440 Gallery.prototype.onOverwriteOriginalClick_ = function(event) { | |
441 var overwrite = event.target.checked; | |
442 localStorage['gallery-overwrite-original'] = overwrite; | |
443 this.applyOverwrite_(overwrite); | |
444 }; | |
445 | |
446 //TODO(JSDOC) | |
447 Gallery.prototype.onCloseBubble_ = function(event) { | |
448 this.bubble_.hidden = true; | |
449 localStorage['gallery-overwrite-bubble'] = Gallery.OVERWRITE_BUBBLE_MAX_TIMES; | |
450 }; | |
451 | |
452 //TODO(JSDOC) | |
453 Gallery.prototype.saveCurrentImage_ = function(callback) { | |
454 var item = this.ribbon_.getSelectedItem(); | |
455 var canvas = this.imageView_.getCanvas(); | |
456 | |
457 this.showSpinner_(true); | |
458 var metadataEncoder = ImageEncoder.encodeMetadata( | |
459 this.selectedImageMetadata_.media, canvas, 1 /* quality */); | |
460 | |
461 this.selectedImageMetadata_ = ContentProvider.ConvertContentMetadata( | |
462 metadataEncoder.getMetadata(), this.selectedImageMetadata_); | |
463 item.setThumbnail(this.selectedImageMetadata_); | |
464 | |
465 item.saveToFile( | |
466 this.context_.saveDirEntry, | |
467 canvas, | |
468 metadataEncoder, | |
469 function(success) { | |
470 // TODO(kaznacheev): Implement write error handling. | |
471 // Until then pretend that the save succeeded. | |
472 this.showSpinner_(false); | |
473 this.flashSavedLabel_(); | |
474 this.metadataCache_.clear(item.getUrl(), Gallery.METADATA_TYPE); | |
475 callback(); | |
476 }.bind(this)); | |
477 }; | |
478 | |
479 //TODO(JSDOC) | |
480 Gallery.prototype.onActionExecute_ = function(action) { | |
481 // |executeWhenReady| closes the sharing menu. | |
482 this.editor_.executeWhenReady(function() { | |
483 action.execute([this.ribbon_.getSelectedItem().getUrl()]); | |
484 }.bind(this)); | |
485 }; | |
486 | |
487 //TODO(JSDOC) | |
488 Gallery.prototype.updateFilename_ = function(opt_url) { | |
489 var fullName; | |
490 | |
491 var item = this.ribbon_.getSelectedItem(); | |
492 if (item) { | |
493 fullName = item.getNameAfterSaving(); | |
494 } else if (opt_url) { | |
495 fullName = ImageUtil.getFullNameFromUrl(opt_url); | |
496 } else { | |
497 return; | |
498 } | |
499 | |
500 this.context_.onNameChange(fullName); | |
501 | |
502 var displayName = ImageUtil.getFileNameFromFullName(fullName); | |
503 this.filenameEdit_.value = displayName; | |
504 this.filenameText_.textContent = displayName; | |
505 }; | |
506 | |
507 /** | |
508 * Click event handler on filename edit box | |
509 * @private | |
510 */ | |
511 Gallery.prototype.onFilenameClick_ = function() { | |
512 // We can't rename files in readonly directory. | |
513 if (this.context_.readonlyDirName) | |
514 return; | |
515 | |
516 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true); | |
517 setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0); | |
518 this.cancelFading_(); | |
519 }; | |
520 | |
521 /** | |
522 * Blur event handler on filename edit box | |
523 * @private | |
524 */ | |
525 Gallery.prototype.onFilenameEditBlur_ = function() { | |
526 if (this.filenameEdit_.value && this.filenameEdit_.value[0] == '.') { | |
527 this.editor_.getPrompt().show('file_hidden_name', 5000); | |
528 this.filenameEdit_.focus(); | |
529 return; | |
530 } | |
531 | |
532 if (this.filenameEdit_.value) { | |
533 this.renameItem_(this.ribbon_.getSelectedItem(), | |
534 this.filenameEdit_.value); | |
535 } | |
536 | |
537 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false); | |
538 this.initiateFading_(); | |
539 }; | |
540 | |
541 /** | |
542 * Keydown event handler on filename edit box | |
543 * @private | |
544 */ | |
545 Gallery.prototype.onFilenameEditKeydown_ = function() { | |
546 switch (event.keyCode) { | |
547 case 27: // Escape | |
548 this.updateFilename_(); | |
549 this.filenameEdit_.blur(); | |
550 break; | |
551 | |
552 case 13: // Enter | |
553 this.filenameEdit_.blur(); | |
554 break; | |
555 } | |
556 event.stopPropagation(); | |
557 }; | |
558 | |
559 //TODO(JSDOC) | |
560 Gallery.prototype.renameItem_ = function(item, name) { | |
561 var dir = this.context_.saveDirEntry; | |
562 var self = this; | |
563 var originalName = item.getNameAfterSaving(); | |
564 if (ImageUtil.getExtensionFromFullName(name) == | |
565 ImageUtil.getExtensionFromFullName(originalName)) { | |
566 name = ImageUtil.getFileNameFromFullName(name); | |
567 } | |
568 var newName = ImageUtil.replaceFileNameInFullName(originalName, name); | |
569 if (originalName == newName) return; | |
570 | |
571 function onError() { | |
572 console.log('Rename error: "' + originalName + '" to "' + newName + '"'); | |
573 } | |
574 | |
575 function onSuccess(entry) { | |
576 item.setUrl(entry.toURL()); | |
577 self.updateFilename_(); | |
578 } | |
579 | |
580 function doRename() { | |
581 if (item.hasNameForSaving()) { | |
582 // Use this name in the next save operation. | |
583 item.setNameForSaving(newName); | |
584 ImageUtil.setAttribute(self.filenameSpacer_, 'overwrite', false); | |
585 self.updateFilename_(); | |
586 } else { | |
587 // Rename file in place. | |
588 dir.getFile( | |
589 ImageUtil.getFullNameFromUrl(item.getUrl()), | |
590 {create: false}, | |
591 function(entry) { entry.moveTo(dir, newName, onSuccess, onError); }, | |
592 onError); | |
593 } | |
594 } | |
595 | |
596 function onVictimFound(victim) { | |
597 self.editor_.getPrompt().show('file_exists', 3000); | |
598 self.filenameEdit_.value = name; | |
599 self.onFilenameClick_(); | |
600 } | |
601 | |
602 dir.getFile(newName, {create: false, exclusive: false}, | |
603 onVictimFound, doRename); | |
604 }; | |
605 | |
606 /** | |
607 * @return {Boolean} True if file renaming is currently in progress | |
608 * @private | |
609 */ | |
610 Gallery.prototype.isRenaming_ = function() { | |
611 return this.filenameSpacer_.hasAttribute('renaming'); | |
612 }; | |
613 | |
614 /** | |
615 * @return {Object} File browser private API. | |
616 */ | |
617 Gallery.getFileBrowserPrivate = function() { | |
618 return chrome.fileBrowserPrivate || window.top.chrome.fileBrowserPrivate; | |
619 }; | |
620 | |
621 /** | |
622 * Switches gallery to fullscreen mode and back | |
623 * @private | |
624 */ | |
625 Gallery.prototype.toggleFullscreen_ = function() { | |
626 Gallery.getFileBrowserPrivate().toggleFullscreen(); | |
627 }; | |
628 | |
629 /** | |
630 * Close the Gallery. | |
631 * @private | |
632 */ | |
633 Gallery.prototype.close_ = function() { | |
634 Gallery.getFileBrowserPrivate().isFullscreen(function(fullscreen) { | |
635 if (this.originalFullscreen_ != fullscreen) { | |
636 Gallery.getFileBrowserPrivate().toggleFullscreen(); | |
637 } | |
638 this.context_.onClose(); | |
639 }.bind(this)); | |
640 }; | |
641 | |
642 /** | |
643 * Handle user's 'Close' action (Escape or a click on the X icon). | |
644 * @private | |
645 */ | |
646 Gallery.prototype.onClose_ = function() { | |
647 // TODO: handle write errors gracefully (suggest retry or saving elsewhere). | |
648 this.editor_.executeWhenReady(this.close_.bind(this)); | |
649 }; | |
650 | |
651 Gallery.prototype.prefetchImage = function(id, url) { | |
652 this.editor_.prefetchImage(id, url); | |
653 }; | |
654 | |
655 Gallery.prototype.openImage = function(id, url, metadata, slide, callback) { | |
656 this.selectedImageMetadata_ = ImageUtil.deepCopy(metadata); | |
657 this.updateFilename_(url); | |
658 if (this.ribbon_.getSelectedItem()) { | |
659 // In edit mode, read overwrite setting. | |
660 // In view mode, don't change the name, so passing |true|. | |
661 if (this.isEditing_()) { | |
662 this.applyDefaultOverwrite_(); | |
663 } else { | |
664 this.applyOverwrite_(true); | |
665 } | |
666 | |
667 if (!this.ribbon_.getSelectedItem().isOriginal()) { | |
668 // For once edited image, do not allow the 'overwrite' setting. | |
669 ImageUtil.setAttribute(this.filenameSpacer_, 'saved', true); | |
670 } else { | |
671 ImageUtil.setAttribute(this.filenameSpacer_, 'saved', false); | |
672 } | |
673 } | |
674 | |
675 this.showSpinner_(true); | |
676 | |
677 var self = this; | |
678 function loadDone(loadType) { | |
679 var video = self.isShowingVideo_(); | |
680 ImageUtil.setAttribute(self.container_, 'video', video); | |
681 | |
682 self.showSpinner_(false); | |
683 if (loadType == ImageView.LOAD_TYPE_ERROR) { | |
684 self.showErrorBanner_(video ? 'VIDEO_ERROR' : 'IMAGE_ERROR'); | |
685 } else if (loadType == ImageView.LOAD_TYPE_OFFLINE) { | |
686 self.showErrorBanner_(video ? 'VIDEO_OFFLINE' : 'IMAGE_OFFLINE'); | |
687 } | |
688 | |
689 if (video) { | |
690 if (self.isEditing_()) { | |
691 // The editor toolbar does not make sense for video, hide it. | |
692 self.onEdit_(); | |
693 } | |
694 self.mediaControls_.attachMedia(self.imageView_.getVideo()); | |
695 //TODO(kaznacheev): Add metrics for video playback. | |
696 } else { | |
697 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View')); | |
698 | |
699 function toMillions(number) { return Math.round(number / (1000 * 1000)) } | |
700 | |
701 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'), | |
702 toMillions(metadata.filesystem.size)); | |
703 | |
704 var canvas = self.imageView_.getCanvas(); | |
705 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'), | |
706 toMillions(canvas.width * canvas.height)); | |
707 | |
708 var extIndex = url.lastIndexOf('.'); | |
709 var ext = extIndex < 0 ? '' : url.substr(extIndex + 1).toLowerCase(); | |
710 if (ext == 'jpeg') ext = 'jpg'; | |
711 ImageUtil.metrics.recordEnum( | |
712 ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES); | |
713 } | |
714 | |
715 callback(loadType); | |
716 } | |
717 | |
718 this.editor_.openSession( | |
719 id, url, metadata, slide, this.saveCurrentImage_.bind(this), loadDone); | |
720 }; | |
721 | |
722 //TODO(JSDOC) | |
723 Gallery.prototype.closeImage = function(callback) { | |
724 this.showSpinner_(false); | |
725 this.showErrorBanner_(false); | |
726 this.editor_.getPrompt().hide(); | |
727 if (this.isShowingVideo_()) { | |
728 this.mediaControls_.pause(); | |
729 this.mediaControls_.detachMedia(); | |
730 } | |
731 this.editor_.closeSession(callback); | |
732 }; | |
733 | |
734 Gallery.prototype.showSpinner_ = function(on) { | |
735 if (this.spinnerTimer_) { | |
736 clearTimeout(this.spinnerTimer_); | |
737 this.spinnerTimer_ = null; | |
738 } | |
739 | |
740 if (on) { | |
741 this.spinnerTimer_ = setTimeout(function() { | |
742 this.spinnerTimer_ = null; | |
743 ImageUtil.setAttribute(this.container_, 'spinner', true); | |
744 }.bind(this), 1000); | |
745 } else { | |
746 ImageUtil.setAttribute(this.container_, 'spinner', false); | |
747 } | |
748 }; | |
749 | |
750 Gallery.prototype.showErrorBanner_ = function(message) { | |
751 if (message) { | |
752 this.errorBanner_.textContent = this.displayStringFunction_(message); | |
753 } | |
754 ImageUtil.setAttribute(this.container_, 'error', !!message); | |
755 }; | |
756 | |
757 Gallery.prototype.isShowingVideo_ = function() { | |
758 return !!this.imageView_.getVideo(); | |
759 }; | |
760 | |
761 Gallery.prototype.saveVideoPosition_ = function() { | |
762 if (this.isShowingVideo_() && this.mediaControls_.isPlaying()) { | |
763 this.mediaControls_.savePosition(); | |
764 } | |
765 }; | |
766 | |
767 Gallery.prototype.onUnload_ = function() { | |
768 this.saveVideoPosition_(); | |
769 window.top.removeEventListener('beforeunload', this.onBeforeUnloadBound_); | |
770 window.top.removeEventListener('unload', this.onTopUnloadBound_); | |
771 }; | |
772 | |
773 Gallery.prototype.onTopUnload_ = function() { | |
774 this.saveVideoPosition_(); | |
775 }; | |
776 | |
777 Gallery.prototype.isEditing_ = function() { | |
778 return this.container_.hasAttribute('editing'); | |
779 }; | |
780 | |
781 Gallery.prototype.onEdit_ = function() { | |
782 ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing_()); | |
783 | |
784 // The user has just clicked on the Edit button. Dismiss the Share menu. | |
785 if (this.isSharing_()) { | |
786 this.onShare_(); | |
787 } | |
788 | |
789 // isEditing_ has just been flipped to a new value. | |
790 if (this.isEditing_()) { | |
791 this.applyDefaultOverwrite_(); | |
792 if (this.context_.readonlyDirName) { | |
793 this.editor_.getPrompt().showAt( | |
794 'top', 'readonly_warning', 0, this.context_.readonlyDirName); | |
795 } | |
796 this.cancelFading_(); | |
797 } else { | |
798 this.applyOverwrite_(true); | |
799 this.editor_.getPrompt().hide(); | |
800 this.initiateFading_(); | |
801 } | |
802 | |
803 ImageUtil.setAttribute(this.editButton_, 'pressed', this.isEditing_()); | |
804 }; | |
805 | |
806 Gallery.prototype.isSharing_ = function(event) { | |
807 return this.shareMode_ && this.shareMode_ == this.editor_.getMode(); | |
808 }; | |
809 | |
810 Gallery.prototype.onShare_ = function(event) { | |
811 this.editor_.enterMode(this.shareMode_, event); | |
812 if (this.isSharing_()) { | |
813 this.cancelFading_(); | |
814 } else { | |
815 this.initiateFading_(); | |
816 } | |
817 }; | |
818 | |
819 Gallery.prototype.onKeyDown_ = function(event) { | |
820 if (this.isEditing_() && this.editor_.onKeyDown(event)) | |
821 return; | |
822 | |
823 switch (util.getKeyModifiers(event) + event.keyIdentifier) { | |
824 case 'U+0008': // Backspace. | |
825 // The default handler would call history.back and close the Gallery. | |
826 event.preventDefault(); | |
827 break; | |
828 | |
829 case 'U+001B': // Escape | |
830 if (this.isEditing_()) { | |
831 this.onEdit_(); | |
832 } else if (this.isSharing_()) { | |
833 this.onShare_(); | |
834 } else { | |
835 this.onClose_(); | |
836 } | |
837 break; | |
838 | |
839 case 'U+0045': // 'e' toggles the editor | |
840 if (!this.isShowingVideo_()) { | |
841 this.onEdit_(); | |
842 } | |
843 break; | |
844 | |
845 case 'U+0020': // Space toggles the video playback. | |
846 if (this.isShowingVideo_()) { | |
847 this.mediaControls_.togglePlayStateWithFeedback(); | |
848 } | |
849 break; | |
850 | |
851 case 'Home': | |
852 this.ribbon_.selectFirst(); | |
853 break; | |
854 case 'Left': | |
855 this.ribbon_.selectNext(-1); | |
856 break; | |
857 case 'Right': | |
858 this.ribbon_.selectNext(1); | |
859 break; | |
860 case 'End': | |
861 this.ribbon_.selectLast(); | |
862 break; | |
863 | |
864 case 'Ctrl-U+00DD': // Ctrl+] (cryptic on purpose). | |
865 this.ribbon_.toggleDebugSlideshow(); | |
866 break; | |
867 } | |
868 }; | |
869 | |
870 Gallery.prototype.onMouseMove_ = function(e) { | |
871 if (this.clientX_ == e.clientX && this.clientY_ == e.clientY) { | |
872 // The mouse has not moved, must be the cursor change triggered by | |
873 // some of the attributes on the root container. Ignore the event. | |
874 return; | |
875 } | |
876 this.clientX_ = e.clientX; | |
877 this.clientY_ = e.clientY; | |
878 | |
879 this.cancelFading_(); | |
880 | |
881 this.mouseOverTool_ = false; | |
882 for (var elem = e.target; elem != this.container_; elem = elem.parentNode) { | |
883 if (elem.classList.contains('tool')) { | |
884 this.mouseOverTool_ = true; | |
885 break; | |
886 } | |
887 } | |
888 | |
889 this.initiateFading_(); | |
890 }; | |
891 | |
892 Gallery.prototype.onFadeTimeout_ = function() { | |
893 this.fadeTimeoutId_ = null; | |
894 if (this.isEditing_() || this.isSharing_() || this.isRenaming_()) return; | |
895 ImageUtil.setAttribute(this.container_, 'tools', false); | |
896 }; | |
897 | |
898 Gallery.prototype.initiateFading_ = function(opt_timeout) { | |
899 if (this.mouseOverTool_ || this.isEditing_() || this.isSharing_() || | |
900 this.isRenaming_()) | |
901 return; | |
902 | |
903 if (!this.fadeTimeoutId_) | |
904 this.fadeTimeoutId_ = window.setTimeout( | |
905 this.onFadeTimeoutBound_, opt_timeout || Gallery.FADE_TIMEOUT); | |
906 }; | |
907 | |
908 Gallery.prototype.cancelFading_ = function() { | |
909 ImageUtil.setAttribute(this.container_, 'tools', true); | |
910 | |
911 if (this.fadeTimeoutId_) { | |
912 window.clearTimeout(this.fadeTimeoutId_); | |
913 this.fadeTimeoutId_ = null; | |
914 } | |
915 }; | |
916 | |
917 //TODO(JSDOC) | |
918 function Ribbon(document, client, metadataCache, arrowLeft, arrowRight) { | |
919 var self = document.createElement('div'); | |
920 Ribbon.decorate(self, client, metadataCache, arrowLeft, arrowRight); | |
921 return self; | |
922 } | |
923 | |
924 Ribbon.prototype.__proto__ = HTMLDivElement.prototype; | |
925 | |
926 //TODO(JSDOC) | |
927 Ribbon.decorate = function( | |
928 self, client, metadataCache, arrowLeft, arrowRight) { | |
929 self.__proto__ = Ribbon.prototype; | |
930 self.client_ = client; | |
931 self.metadataCache_ = metadataCache; | |
932 | |
933 self.items_ = []; | |
934 self.selectedIndex_ = -1; | |
935 | |
936 self.arrowLeft_ = arrowLeft; | |
937 self.arrowLeft_. | |
938 addEventListener('click', self.selectNext.bind(self, -1, null)); | |
939 | |
940 self.arrowRight_ = arrowRight; | |
941 self.arrowRight_. | |
942 addEventListener('click', self.selectNext.bind(self, 1, null)); | |
943 | |
944 self.className = 'ribbon'; | |
945 }; | |
946 | |
947 //TODO(JSDOC) | |
948 Ribbon.PAGING_SINGLE_ITEM_DELAY = 20; | |
949 | |
950 //TODO(JSDOC) | |
951 Ribbon.PAGING_ANIMATION_DURATION = 200; | |
952 | |
953 /** | |
954 * @return {Ribbon.Item?} The selected item. | |
955 */ | |
956 Ribbon.prototype.getSelectedItem = function() { | |
957 return this.items_[this.selectedIndex_]; | |
958 }; | |
959 | |
960 //TODO(JSDOC) | |
961 Ribbon.prototype.clear = function() { | |
962 this.textContent = ''; | |
963 this.items_ = []; | |
964 this.selectedIndex_ = -1; | |
965 this.firstVisibleIndex_ = 0; | |
966 this.lastVisibleIndex_ = -1; // Zero thumbnails | |
967 this.sequenceDirection_ = 0; | |
968 this.sequenceLength_ = 0; | |
969 }; | |
970 | |
971 //TODO(JSDOC) | |
972 Ribbon.prototype.add = function(url) { | |
973 var index = this.items_.length; | |
974 var item = new Ribbon.Item(this.ownerDocument, index, url); | |
975 item.addEventListener('click', this.select.bind(this, index, 0, null)); | |
976 this.items_.push(item); | |
977 }; | |
978 | |
979 //TODO(JSDOC) | |
980 Ribbon.prototype.load = function(urls, selectedIndex) { | |
981 this.clear(); | |
982 for (var index = 0; index < urls.length; ++index) { | |
983 this.add(urls[index]); | |
984 } | |
985 this.selectedIndex_ = selectedIndex; | |
986 | |
987 // We do not want to call this.select because the selected image is already | |
988 // displayed. Instead we just update the UI. | |
989 this.getSelectedItem().select(true); | |
990 this.redraw(); | |
991 | |
992 // Let the thumbnails load before prefetching the next image. | |
993 setTimeout(this.requestPrefetch.bind(this, 1), 1000); | |
994 | |
995 // Make the arrows visible if there are more than 1 image. | |
996 ImageUtil.setAttribute(this.arrowLeft_, 'active', this.items_.length > 1); | |
997 ImageUtil.setAttribute(this.arrowRight_, 'active', this.items_.length > 1); | |
998 }; | |
999 | |
1000 //TODO(JSDOC) | |
1001 Ribbon.prototype.select = function(index, opt_forceStep, opt_callback) { | |
1002 if (index == this.selectedIndex_) | |
1003 return; // Do not reselect. | |
1004 | |
1005 this.client_.closeImage( | |
1006 this.doSelect_.bind(this, index, opt_forceStep, opt_callback)); | |
1007 }; | |
1008 | |
1009 //TODO(JSDOC) | |
1010 Ribbon.prototype.doSelect_ = function(index, opt_forceStep, opt_callback) { | |
1011 if (index == this.selectedIndex_) | |
1012 return; // Do not reselect | |
1013 | |
1014 var selectedItem = this.getSelectedItem(); | |
1015 selectedItem.select(false); | |
1016 | |
1017 var step = opt_forceStep || (index - this.selectedIndex_); | |
1018 | |
1019 if (Math.abs(step) != 1) { | |
1020 // Long leap, the sequence is broken, we have no good prefetch candidate. | |
1021 this.sequenceDirection_ = 0; | |
1022 this.sequenceLength_ = 0; | |
1023 } else if (this.sequenceDirection_ == step) { | |
1024 // Keeping going in sequence. | |
1025 this.sequenceLength_++; | |
1026 } else { | |
1027 // Reversed the direction. Reset the counter. | |
1028 this.sequenceDirection_ = step; | |
1029 this.sequenceLength_ = 1; | |
1030 } | |
1031 | |
1032 if (this.sequenceLength_ <= 1) { | |
1033 // We have just broke the sequence. Touch the current image so that it stays | |
1034 // in the cache longer. | |
1035 this.client_.prefetchImage(selectedItem.getIndex(), selectedItem.getUrl()); | |
1036 } | |
1037 | |
1038 this.selectedIndex_ = index; | |
1039 | |
1040 selectedItem = this.getSelectedItem(); | |
1041 selectedItem.select(true); | |
1042 this.redraw(); | |
1043 | |
1044 function shouldPrefetch(loadType, step, sequenceLength) { | |
1045 // Never prefetch when selecting out of sequence. | |
1046 if (Math.abs(step) != 1) | |
1047 return false; | |
1048 | |
1049 // Never prefetch after a video load (decoding the next image can freeze | |
1050 // the UI for a second or two). | |
1051 if (loadType == ImageView.LOAD_TYPE_VIDEO_FILE) | |
1052 return false; | |
1053 | |
1054 // Always prefetch if the previous load was from cache. | |
1055 if (loadType == ImageView.LOAD_TYPE_CACHED_FULL) | |
1056 return true; | |
1057 | |
1058 // Prefetch if we have been going in the same direction for long enough. | |
1059 return sequenceLength >= 3; | |
1060 } | |
1061 | |
1062 var self = this; | |
1063 function onMetadata(metadata) { | |
1064 if (!selectedItem.isSelected()) return; | |
1065 self.client_.openImage( | |
1066 selectedItem.getIndex(), selectedItem.getUrl(), metadata, step, | |
1067 function(loadType) { | |
1068 if (!selectedItem.isSelected()) return; | |
1069 if (shouldPrefetch(loadType, step, self.sequenceLength_)) { | |
1070 self.requestPrefetch(step); | |
1071 } | |
1072 if (opt_callback) opt_callback(); | |
1073 }); | |
1074 } | |
1075 this.metadataCache_.get(selectedItem.getUrl(), | |
1076 Gallery.METADATA_TYPE, onMetadata); | |
1077 }; | |
1078 | |
1079 //TODO(JSDOC) | |
1080 Ribbon.prototype.requestPrefetch = function(direction) { | |
1081 if (this.items_.length < 2) return; | |
1082 | |
1083 var index = this.getNextSelectedIndex_(direction); | |
1084 var nextItemUrl = this.items_[index].getUrl(); | |
1085 | |
1086 var selectedItem = this.getSelectedItem(); | |
1087 this.metadataCache_.get(nextItemUrl, Gallery.METADATA_TYPE, | |
1088 function(metadata) { | |
1089 if (!selectedItem.isSelected()) return; | |
1090 this.client_.prefetchImage(index, nextItemUrl, metadata); | |
1091 }.bind(this)); | |
1092 }; | |
1093 | |
1094 //TODO(JSDOC) | |
1095 Ribbon.ITEMS_COUNT = 5; | |
1096 | |
1097 //TODO(JSDOC) | |
1098 Ribbon.prototype.redraw = function() { | |
1099 // Never show a single thumbnail. | |
1100 if (this.items_.length == 1) | |
1101 return; | |
1102 | |
1103 var initThumbnail = function(index) { | |
1104 var item = this.items_[index]; | |
1105 if (!item.hasThumbnail()) | |
1106 this.metadataCache_.get(item.getUrl(), Gallery.METADATA_TYPE, | |
1107 item.setThumbnail.bind(item)); | |
1108 }.bind(this); | |
1109 | |
1110 // TODO(dgozman): use margin instead of 2 here. | |
1111 var itemWidth = this.clientHeight - 2; | |
1112 var fullItems = Ribbon.ITEMS_COUNT; | |
1113 fullItems = Math.min(fullItems, this.items_.length); | |
1114 var right = Math.floor((fullItems - 1) / 2); | |
1115 | |
1116 var fullWidth = fullItems * itemWidth; | |
1117 this.style.width = fullWidth + 'px'; | |
1118 | |
1119 var lastIndex = this.selectedIndex_ + right; | |
1120 lastIndex = Math.max(lastIndex, fullItems - 1); | |
1121 lastIndex = Math.min(lastIndex, this.items_.length - 1); | |
1122 var firstIndex = lastIndex - fullItems + 1; | |
1123 | |
1124 if (this.firstVisibleIndex_ == firstIndex && | |
1125 this.lastVisibleIndex_ == lastIndex) { | |
1126 return; | |
1127 } | |
1128 | |
1129 if (this.lastVisibleIndex_ == -1) { | |
1130 this.firstVisibleIndex_ = firstIndex; | |
1131 this.lastVisibleIndex_ = lastIndex; | |
1132 } | |
1133 | |
1134 this.textContent = ''; | |
1135 var startIndex = Math.min(firstIndex, this.firstVisibleIndex_); | |
1136 var toRemove = []; | |
1137 // All the items except the first one treated equally. | |
1138 for (var index = startIndex + 1; | |
1139 index <= Math.max(lastIndex, this.lastVisibleIndex_); | |
1140 ++index) { | |
1141 initThumbnail(index); | |
1142 var box = this.items_[index]; | |
1143 box.style.marginLeft = '0'; | |
1144 this.appendChild(box); | |
1145 if (index < firstIndex || index > lastIndex) { | |
1146 toRemove.push(index); | |
1147 } | |
1148 } | |
1149 | |
1150 var margin = itemWidth * Math.abs(firstIndex - this.firstVisibleIndex_); | |
1151 initThumbnail(startIndex); | |
1152 var startBox = this.items_[startIndex]; | |
1153 if (startIndex == firstIndex) { | |
1154 // Sliding to the right. | |
1155 startBox.style.marginLeft = -margin + 'px'; | |
1156 if (this.firstChild) | |
1157 this.insertBefore(startBox, this.firstChild); | |
1158 else | |
1159 this.appendChild(startBox); | |
1160 setTimeout(function() { | |
1161 startBox.style.marginLeft = '0'; | |
1162 }, 0); | |
1163 } else { | |
1164 // Sliding to the left. Start item will become invisible and should be | |
1165 // removed afterwards. | |
1166 toRemove.push(startIndex); | |
1167 startBox.style.marginLeft = '0'; | |
1168 if (this.firstChild) | |
1169 this.insertBefore(startBox, this.firstChild); | |
1170 else | |
1171 this.appendChild(startBox); | |
1172 setTimeout(function() { | |
1173 startBox.style.marginLeft = -margin + 'px'; | |
1174 }, 0); | |
1175 } | |
1176 | |
1177 ImageUtil.setClass(this, 'fade-left', | |
1178 firstIndex > 0 && this.selectedIndex_ != firstIndex); | |
1179 | |
1180 ImageUtil.setClass(this, 'fade-right', | |
1181 lastIndex < this.items_.length - 1 && this.selectedIndex_ != lastIndex); | |
1182 | |
1183 this.firstVisibleIndex_ = firstIndex; | |
1184 this.lastVisibleIndex_ = lastIndex; | |
1185 | |
1186 setTimeout(function() { | |
1187 for (var i = 0; i < toRemove.length; i++) { | |
1188 var index = toRemove[i]; | |
1189 if (index < this.firstVisibleIndex_ || index > this.lastVisibleIndex_) { | |
1190 var box = this.items_[index]; | |
1191 if (box.parentNode == this) | |
1192 this.removeChild(box); | |
1193 } | |
1194 } | |
1195 }.bind(this), 200); | |
1196 }; | |
1197 | |
1198 //TODO(JSDOC) | |
1199 Ribbon.prototype.getNextSelectedIndex_ = function(direction) { | |
1200 var index = this.selectedIndex_ + (direction > 0 ? 1 : -1); | |
1201 if (index == -1) return this.items_.length - 1; | |
1202 if (index == this.items_.length) return 0; | |
1203 return index; | |
1204 }; | |
1205 | |
1206 //TODO(JSDOC) | |
1207 Ribbon.prototype.selectNext = function(direction, opt_callback) { | |
1208 this.select(this.getNextSelectedIndex_(direction), direction, opt_callback); | |
1209 }; | |
1210 | |
1211 //TODO(JSDOC) | |
1212 Ribbon.prototype.selectFirst = function() { | |
1213 this.select(0); | |
1214 }; | |
1215 | |
1216 //TODO(JSDOC) | |
1217 Ribbon.prototype.selectLast = function() { | |
1218 this.select(this.items_.length - 1); | |
1219 }; | |
1220 | |
1221 /** | |
1222 * Start/stop the slide show. This is useful for performance debugging and | |
1223 * available only through a cryptic keyboard shortcut. | |
1224 */ | |
1225 Ribbon.prototype.toggleDebugSlideshow = function() { | |
1226 if (this.slideShowTimeout_) { | |
1227 clearInterval(this.slideShowTimeout_); | |
1228 this.slideShowTimeout_ = null; | |
1229 } else { | |
1230 var self = this; | |
1231 function nextSlide() { | |
1232 self.selectNext(1, | |
1233 function() { self.slideShowTimeout_ = setTimeout(nextSlide, 5000) }); | |
1234 } | |
1235 nextSlide(); | |
1236 } | |
1237 }; | |
1238 | |
1239 //TODO(JSDOC) | |
1240 Ribbon.Item = function(document, index, url) { | |
1241 var self = document.createElement('div'); | |
1242 Ribbon.Item.decorate(self, index, url); | |
1243 return self; | |
1244 }; | |
1245 | |
1246 Ribbon.Item.prototype.__proto__ = HTMLDivElement.prototype; | |
1247 | |
1248 //TODO(JSDOC) | |
1249 Ribbon.Item.decorate = function(self, index, url, selectClosure) { | |
1250 self.__proto__ = Ribbon.Item.prototype; | |
1251 self.index_ = index; | |
1252 self.url_ = url; | |
1253 | |
1254 self.className = 'ribbon-image'; | |
1255 | |
1256 var wrapper = self.ownerDocument.createElement('div'); | |
1257 wrapper.className = 'image-wrapper'; | |
1258 self.appendChild(wrapper); | |
1259 | |
1260 self.original_ = true; | |
1261 self.nameForSaving_ = null; | |
1262 }; | |
1263 | |
1264 //TODO(JSDOC) | |
1265 Ribbon.Item.prototype.getIndex = function() { return this.index_ }; | |
1266 | |
1267 //TODO(JSDOC) | |
1268 Ribbon.Item.prototype.isOriginal = function() { return this.original_ }; | |
1269 | |
1270 //TODO(JSDOC) | |
1271 Ribbon.Item.prototype.getUrl = function() { return this.url_ }; | |
1272 | |
1273 //TODO(JSDOC) | |
1274 Ribbon.Item.prototype.setUrl = function(url) { this.url_ = url }; | |
1275 | |
1276 //TODO(JSDOC) | |
1277 Ribbon.Item.prototype.getNameAfterSaving = function() { | |
1278 return this.nameForSaving_ || ImageUtil.getFullNameFromUrl(this.url_); | |
1279 }; | |
1280 | |
1281 //TODO(JSDOC) | |
1282 Ribbon.Item.prototype.hasNameForSaving = function() { | |
1283 return !!this.nameForSaving_; | |
1284 }; | |
1285 | |
1286 //TODO(JSDOC) | |
1287 Ribbon.Item.prototype.isSelected = function() { | |
1288 return this.hasAttribute('selected'); | |
1289 }; | |
1290 | |
1291 //TODO(JSDOC) | |
1292 Ribbon.Item.prototype.select = function(on) { | |
1293 ImageUtil.setAttribute(this, 'selected', on); | |
1294 }; | |
1295 | |
1296 //TODO(JSDOC) | |
1297 Ribbon.Item.prototype.saveToFile = function( | |
1298 dirEntry, canvas, metadataEncoder, opt_callback) { | |
1299 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('SaveTime')); | |
1300 | |
1301 var self = this; | |
1302 | |
1303 var name = this.getNameAfterSaving(); | |
1304 // If we do overwrite original, keep this one as 'original'. | |
1305 this.original_ = this.nameForSaving_ == null; | |
1306 this.nameForSaving_ = null; | |
1307 | |
1308 function onSuccess(url) { | |
1309 console.log('Saved from gallery', name); | |
1310 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 1, 2); | |
1311 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('SaveTime')); | |
1312 self.setUrl(url); | |
1313 if (opt_callback) opt_callback(true); | |
1314 } | |
1315 | |
1316 function onError(error) { | |
1317 console.log('Error saving from gallery', name, error); | |
1318 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 0, 2); | |
1319 if (opt_callback) opt_callback(false); | |
1320 } | |
1321 | |
1322 function doSave(newFile, fileEntry) { | |
1323 fileEntry.createWriter(function(fileWriter) { | |
1324 function writeContent() { | |
1325 fileWriter.onwriteend = onSuccess.bind(null, fileEntry.toURL()); | |
1326 fileWriter.write(ImageEncoder.getBlob(canvas, metadataEncoder)); | |
1327 } | |
1328 fileWriter.onerror = function(error) { | |
1329 onError(error); | |
1330 // Disable all callbacks on the first error. | |
1331 fileWriter.onerror = null; | |
1332 fileWriter.onwriteend = null; | |
1333 }; | |
1334 if (newFile) { | |
1335 writeContent(); | |
1336 } else { | |
1337 fileWriter.onwriteend = writeContent; | |
1338 fileWriter.truncate(0); | |
1339 } | |
1340 }, onError); | |
1341 } | |
1342 | |
1343 function getFile(newFile) { | |
1344 dirEntry.getFile(name, {create: newFile, exclusive: newFile}, | |
1345 doSave.bind(null, newFile), onError); | |
1346 } | |
1347 | |
1348 dirEntry.getFile(name, {create: false, exclusive: false}, | |
1349 getFile.bind(null, false /* existing file */), | |
1350 getFile.bind(null, true /* create new file */)); | |
1351 }; | |
1352 | |
1353 // TODO: Localize? | |
1354 //TODO(JSDOC) | |
1355 Ribbon.Item.COPY_SIGNATURE = 'Edited'; | |
1356 | |
1357 //TODO(JSDOC) | |
1358 Ribbon.Item.REGEXP_COPY_N = | |
1359 new RegExp('^' + Ribbon.Item.COPY_SIGNATURE + ' \\((\\d+)\\)( - .+)$'); | |
1360 | |
1361 //TODO(JSDOC) | |
1362 Ribbon.Item.REGEXP_COPY_0 = | |
1363 new RegExp('^' + Ribbon.Item.COPY_SIGNATURE + '( - .+)$'); | |
1364 | |
1365 //TODO(JSDOC) | |
1366 Ribbon.Item.prototype.createCopyName_ = function(dirEntry, metadata, callback) { | |
1367 var name = ImageUtil.getFullNameFromUrl(this.url_); | |
1368 | |
1369 // If the item represents a file created during the current Gallery session | |
1370 // we reuse it for subsequent saves instead of creating multiple copies. | |
1371 if (!this.original_) | |
1372 return name; | |
1373 | |
1374 var ext = ''; | |
1375 var index = name.lastIndexOf('.'); | |
1376 if (index != -1) { | |
1377 ext = name.substr(index); | |
1378 name = name.substr(0, index); | |
1379 } | |
1380 | |
1381 var mimeType = metadata.media && metadata.media.mimeType; | |
1382 mimeType = (mimeType || '').toLowerCase(); | |
1383 if (mimeType != 'image/jpeg') { | |
1384 // Chrome can natively encode only two formats: JPEG and PNG. | |
1385 // All non-JPEG images are saved in PNG, hence forcing the file extension. | |
1386 ext = '.png'; | |
1387 } | |
1388 | |
1389 function tryNext(tries) { | |
1390 // All the names are used. Let's overwrite the last one. | |
1391 if (tries == 0) { | |
1392 setTimeout(callback, 0, name + ext); | |
1393 return; | |
1394 } | |
1395 | |
1396 // If the file name contains the copy signature add/advance the sequential | |
1397 // number. | |
1398 var matchN = Ribbon.Item.REGEXP_COPY_N.exec(name); | |
1399 var match0 = Ribbon.Item.REGEXP_COPY_0.exec(name); | |
1400 if (matchN && matchN[1] && matchN[2]) { | |
1401 var copyNumber = parseInt(matchN[1], 10) + 1; | |
1402 name = Ribbon.Item.COPY_SIGNATURE + ' (' + copyNumber + ')' + matchN[2]; | |
1403 } else if (match0 && match0[1]) { | |
1404 name = Ribbon.Item.COPY_SIGNATURE + ' (1)' + match0[1]; | |
1405 } else { | |
1406 name = Ribbon.Item.COPY_SIGNATURE + ' - ' + name; | |
1407 } | |
1408 | |
1409 dirEntry.getFile(name + ext, {create: false, exclusive: false}, | |
1410 tryNext.bind(null, tries - 1), | |
1411 callback.bind(null, name + ext)); | |
1412 } | |
1413 | |
1414 tryNext(10); | |
1415 }; | |
1416 | |
1417 //TODO(JSDOC) | |
1418 Ribbon.Item.prototype.setCopyName = function(dirEntry, metadata, opt_callback) { | |
1419 this.createCopyName_(dirEntry, metadata, function(name) { | |
1420 this.nameForSaving_ = name; | |
1421 if (opt_callback) opt_callback(); | |
1422 }.bind(this)); | |
1423 }; | |
1424 | |
1425 //TODO(JSDOC) | |
1426 Ribbon.Item.prototype.setOriginalName = function(dirEntry, opt_callback) { | |
1427 this.nameForSaving_ = null; | |
1428 if (opt_callback) opt_callback(); | |
1429 }; | |
1430 | |
1431 //TODO(JSDOC) | |
1432 Ribbon.Item.prototype.setNameForSaving = function(newName) { | |
1433 this.nameForSaving_ = newName; | |
1434 }; | |
1435 | |
1436 //TODO(JSDOC) | |
1437 Ribbon.Item.prototype.hasThumbnail = function() { | |
1438 return !!this.querySelector('img[src]'); | |
1439 }; | |
1440 | |
1441 //TODO(JSDOC) | |
1442 Ribbon.Item.prototype.setThumbnail = function(metadata) { | |
1443 new ThumbnailLoader(this.url_, metadata). | |
1444 load(this.querySelector('.image-wrapper'), true /* fill */); | |
1445 }; | |
1446 | |
1447 //TODO(JSDOC) | |
1448 function ShareMode(editor, container, toolbar, shareActions, | |
1449 onClick, actionCallback, displayStringFunction) { | |
1450 ImageEditor.Mode.call(this, 'share'); | |
1451 | |
1452 this.message_ = null; | |
1453 | |
1454 var doc = container.ownerDocument; | |
1455 var button = doc.createElement('div'); | |
1456 button.className = 'button share'; | |
1457 button.textContent = displayStringFunction('share'); | |
1458 button.addEventListener('click', onClick); | |
1459 toolbar.appendChild(button); | |
1460 | |
1461 this.bind(editor, button); | |
1462 | |
1463 this.menu_ = doc.createElement('div'); | |
1464 this.menu_.className = 'share-menu'; | |
1465 this.menu_.hidden = true; | |
1466 for (var index = 0; index < shareActions.length; index++) { | |
1467 var action = shareActions[index]; | |
1468 var row = doc.createElement('div'); | |
1469 var img = doc.createElement('img'); | |
1470 img.src = action.iconUrl; | |
1471 row.appendChild(img); | |
1472 row.appendChild(doc.createTextNode(action.title)); | |
1473 row.addEventListener('click', actionCallback.bind(null, action)); | |
1474 this.menu_.appendChild(row); | |
1475 } | |
1476 var arrow = doc.createElement('div'); | |
1477 arrow.className = 'bubble-point'; | |
1478 this.menu_.appendChild(arrow); | |
1479 container.appendChild(this.menu_); | |
1480 } | |
1481 | |
1482 ShareMode.prototype = { __proto__: ImageEditor.Mode.prototype }; | |
1483 | |
1484 /** | |
1485 * Shows share mode UI. | |
1486 */ | |
1487 ShareMode.prototype.setUp = function() { | |
1488 ImageEditor.Mode.prototype.setUp.apply(this, arguments); | |
1489 this.menu_.hidden = false; | |
1490 ImageUtil.setAttribute(this.button_, 'pressed', false); | |
1491 }; | |
1492 | |
1493 /** | |
1494 * Hides share mode UI. | |
1495 */ | |
1496 ShareMode.prototype.cleanUpUI = function() { | |
1497 ImageEditor.Mode.prototype.cleanUpUI.apply(this, arguments); | |
1498 this.menu_.hidden = true; | |
1499 }; | |
1500 | |
1501 /** | |
1502 * Overlay that handles swipe gestures. Changes to the next or previous file. | |
1503 * @param {Ribbon} ribbon Ribbon to handle swipes. | |
1504 * @constructor | |
1505 * @implements {ImageBuffer.Overlay} | |
1506 */ | |
1507 function SwipeOverlay(ribbon) { | |
1508 this.ribbon_ = ribbon; | |
1509 } | |
1510 | |
1511 SwipeOverlay.prototype.__proto__ = ImageBuffer.Overlay.prototype; | |
1512 | |
1513 /** | |
1514 * @param {number} x X pointer position. | |
1515 * @param {number} y Y pointer position. | |
1516 * @param {boolean} touch True if dragging caused by touch. | |
1517 * @return {function} The closure to call on drag. | |
1518 */ | |
1519 SwipeOverlay.prototype.getDragHandler = function(x, y, touch) { | |
1520 if (!touch) | |
1521 return null; | |
1522 var origin = x; | |
1523 var done = false; | |
1524 var self = this; | |
1525 return function(x, y) { | |
1526 if (!done && origin - x > SwipeOverlay.SWIPE_THRESHOLD) { | |
1527 self.handleSwipe_(1); | |
1528 done = true; | |
1529 } else if (!done && x - origin > SwipeOverlay.SWIPE_THRESHOLD) { | |
1530 self.handleSwipe_(-1); | |
1531 done = true; | |
1532 } | |
1533 }; | |
1534 }; | |
1535 | |
1536 /** | |
1537 * Called when the swipe gesture is recognized. | |
1538 * @param {number} direction 1 means swipe to left, -1 swipe to right. | |
1539 * @private | |
1540 */ | |
1541 SwipeOverlay.prototype.handleSwipe_ = function(direction) { | |
1542 this.ribbon_.selectNext(direction); | |
1543 }; | |
1544 | |
1545 /** | |
1546 * If the user touched the image and moved the finger more than SWIPE_THRESHOLD | |
1547 * horizontally it's considered as a swipe gesture (change the current image). | |
1548 */ | |
1549 SwipeOverlay.SWIPE_THRESHOLD = 100; | |
OLD | NEW |