Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(358)

Side by Side Diff: chrome/browser/resources/file_manager/js/image_editor/gallery.js

Issue 10834354: Refactor the Photo Editor to enable new feature work (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Rebase Created 8 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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;
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698