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 * Slide mode displays a single image and has a set of controls to navigate |
| 7 * between the images and to edit an image. |
| 8 * |
| 9 * @param {Element} container Container element. |
| 10 * @param {Element} toolbar Toolbar element. |
| 11 * @param {ImageEditor.Prompt} prompt Prompt. |
| 12 * @param {Object} context Context. |
| 13 * @param {function(string):string} displayStringFunction String formatting |
| 14 * function. |
| 15 * @constructor |
| 16 */ |
| 17 function SlideMode(container, toolbar, prompt, context, displayStringFunction) { |
| 18 this.container_ = container; |
| 19 this.toolbar_ = toolbar; |
| 20 this.document_ = container.ownerDocument; |
| 21 this.prompt_ = prompt; |
| 22 this.context_ = context; |
| 23 this.metadataCache_ = context.metadataCache; |
| 24 this.displayStringFunction_ = displayStringFunction; |
| 25 |
| 26 this.initListeners_(); |
| 27 this.initDom_(); |
| 28 } |
| 29 |
| 30 /** |
| 31 * SlideMode extends cr.EventTarget. |
| 32 */ |
| 33 SlideMode.prototype.__proto__ = cr.EventTarget.prototype; |
| 34 |
| 35 /** |
| 36 * List of available editor modes. |
| 37 * @type {Array.<ImageEditor.Mode>} |
| 38 */ |
| 39 SlideMode.editorModes = [ |
| 40 new ImageEditor.Mode.InstantAutofix(), |
| 41 new ImageEditor.Mode.Crop(), |
| 42 new ImageEditor.Mode.Exposure(), |
| 43 new ImageEditor.Mode.OneClick('rotate_left', new Command.Rotate(-1)), |
| 44 new ImageEditor.Mode.OneClick('rotate_right', new Command.Rotate(1)) |
| 45 ]; |
| 46 |
| 47 /** |
| 48 * Initialize the listeners. |
| 49 * @private |
| 50 */ |
| 51 SlideMode.prototype.initListeners_ = function() { |
| 52 var win = this.document_.defaultView; |
| 53 |
| 54 this.onBeforeUnloadBound_ = this.onBeforeUnload_.bind(this); |
| 55 win.top.addEventListener('beforeunload', this.onBeforeUnloadBound_); |
| 56 |
| 57 win.addEventListener('unload', this.onUnload_.bind(this)); |
| 58 |
| 59 // We need to listen to the top window 'unload' and 'beforeunload' because |
| 60 // the Gallery iframe does not get notified if the tab is closed. |
| 61 this.onTopUnloadBound_ = this.onTopUnload_.bind(this); |
| 62 win.top.addEventListener('unload', this.onTopUnloadBound_); |
| 63 |
| 64 win.addEventListener('resize', this.onResize_.bind(this), false); |
| 65 }; |
| 66 |
| 67 /** |
| 68 * Initialize the UI. |
| 69 * @private |
| 70 */ |
| 71 SlideMode.prototype.initDom_ = function() { |
| 72 // Container for displayed image or video. |
| 73 this.imageContainer_ = util.createChild( |
| 74 this.document_.querySelector('.content'), 'image-container'); |
| 75 this.imageContainer_.addEventListener('click', this.onClick_.bind(this)); |
| 76 |
| 77 // Overwrite options and info bubble. |
| 78 this.options_ = util.createChild( |
| 79 this.toolbar_.querySelector('.filename-spacer'), 'options'); |
| 80 |
| 81 this.savedLabel_ = util.createChild(this.options_, 'saved'); |
| 82 this.savedLabel_.textContent = this.displayStringFunction_('saved'); |
| 83 |
| 84 var overwriteOriginalBox = |
| 85 util.createChild(this.options_, 'overwrite-original'); |
| 86 |
| 87 this.overwriteOriginal_ = util.createChild( |
| 88 overwriteOriginalBox, 'common white', 'input'); |
| 89 this.overwriteOriginal_.type = 'checkbox'; |
| 90 this.overwriteOriginal_.id = 'overwrite-checkbox'; |
| 91 this.overwriteOriginal_.checked = this.shouldOverwriteOriginal_(); |
| 92 this.overwriteOriginal_.addEventListener('click', |
| 93 this.onOverwriteOriginalClick_.bind(this)); |
| 94 |
| 95 var overwriteLabel = util.createChild(overwriteOriginalBox, '', 'label'); |
| 96 overwriteLabel.textContent = |
| 97 this.displayStringFunction_('overwrite_original'); |
| 98 overwriteLabel.setAttribute('for', 'overwrite-checkbox'); |
| 99 |
| 100 this.bubble_ = util.createChild(this.toolbar_, 'bubble'); |
| 101 this.bubble_.hidden = true; |
| 102 |
| 103 var bubbleContent = util.createChild(this.bubble_); |
| 104 bubbleContent.innerHTML = this.displayStringFunction_('overwrite_bubble'); |
| 105 |
| 106 util.createChild(this.bubble_, 'pointer bottom', 'span'); |
| 107 |
| 108 var bubbleClose = util.createChild(this.bubble_, 'close-x'); |
| 109 bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this)); |
| 110 |
| 111 // Video player controls. |
| 112 this.mediaSpacer_ = |
| 113 util.createChild(this.container_, 'video-controls-spacer'); |
| 114 this.mediaToolbar_ = util.createChild(this.mediaSpacer_, 'tool'); |
| 115 this.mediaControls_ = new VideoControls( |
| 116 this.mediaToolbar_, |
| 117 this.showErrorBanner_.bind(this, 'VIDEO_ERROR'), |
| 118 Gallery.toggleFullscreen, |
| 119 this.container_); |
| 120 |
| 121 // Ribbon and related controls. |
| 122 this.arrowBox_ = util.createChild(this.container_, 'arrow-box'); |
| 123 |
| 124 this.arrowLeft_ = |
| 125 util.createChild(this.arrowBox_, 'arrow left tool dimmable'); |
| 126 this.arrowLeft_.addEventListener('click', |
| 127 this.selectNext.bind(this, -1, null)); |
| 128 util.createChild(this.arrowLeft_); |
| 129 |
| 130 util.createChild(this.arrowBox_, 'arrow-spacer'); |
| 131 |
| 132 this.arrowRight_ = |
| 133 util.createChild(this.arrowBox_, 'arrow right tool dimmable'); |
| 134 this.arrowRight_.addEventListener('click', |
| 135 this.selectNext.bind(this, 1, null)); |
| 136 util.createChild(this.arrowRight_); |
| 137 |
| 138 this.ribbonSpacer_ = util.createChild(this.toolbar_, 'ribbon-spacer'); |
| 139 |
| 140 // Error indicator. |
| 141 var errorWrapper = util.createChild(this.container_, 'prompt-wrapper'); |
| 142 errorWrapper.setAttribute('pos', 'center'); |
| 143 |
| 144 this.errorBanner_ = util.createChild(errorWrapper, 'error-banner'); |
| 145 |
| 146 util.createChild(this.container_, 'spinner'); |
| 147 |
| 148 |
| 149 this.editButton_ = util.createChild(this.toolbar_, 'button edit'); |
| 150 this.editButton_.textContent = this.displayStringFunction_('edit'); |
| 151 this.editButton_.addEventListener('click', this.onEdit_.bind(this)); |
| 152 |
| 153 // Editor toolbar. |
| 154 |
| 155 this.editBar_ = util.createChild(this.toolbar_, 'edit-bar'); |
| 156 this.editBarMain_ = util.createChild(this.editBar_, 'edit-main'); |
| 157 |
| 158 this.editBarMode_ = util.createChild(this.container_, 'edit-modal'); |
| 159 this.editBarModeWrapper_ = util.createChild( |
| 160 this.editBarMode_, 'edit-modal-wrapper'); |
| 161 this.editBarModeWrapper_.hidden = true; |
| 162 |
| 163 // Objects supporting image display and editing. |
| 164 this.viewport_ = new Viewport(); |
| 165 |
| 166 this.imageView_ = new ImageView( |
| 167 this.imageContainer_, |
| 168 this.viewport_, |
| 169 this.metadataCache_); |
| 170 |
| 171 this.imageView_.addContentCallback(this.onImageContentChanged_.bind(this)); |
| 172 |
| 173 this.editor_ = new ImageEditor( |
| 174 this.viewport_, |
| 175 this.imageView_, |
| 176 this.prompt_, |
| 177 { |
| 178 root: this.container_, |
| 179 image: this.imageContainer_, |
| 180 toolbar: this.editBarMain_, |
| 181 mode: this.editBarModeWrapper_ |
| 182 }, |
| 183 SlideMode.editorModes, |
| 184 this.displayStringFunction_); |
| 185 |
| 186 this.editor_.getBuffer().addOverlay( |
| 187 new SwipeOverlay(this.selectNext.bind(this))); |
| 188 }; |
| 189 |
| 190 /** |
| 191 * Load items, display the selected item. |
| 192 * |
| 193 * @param {Array.<Gallery.Item>} items Array of items. |
| 194 * @param {number} selectedIndex Selected index. |
| 195 * @param {function} callback Callback. |
| 196 */ |
| 197 SlideMode.prototype.load = function(items, selectedIndex, callback) { |
| 198 var selectedItem = items[selectedIndex]; |
| 199 if (!selectedItem) { |
| 200 this.showErrorBanner_('IMAGE_ERROR'); |
| 201 return; |
| 202 } |
| 203 |
| 204 var loadDone = function() { |
| 205 this.items_ = items; |
| 206 this.setSelectedIndex_(selectedIndex); |
| 207 this.setupNavigation_(); |
| 208 setTimeout(this.requestPrefetch.bind(this, 1 /* Next item */), 1000); |
| 209 callback(); |
| 210 }.bind(this); |
| 211 |
| 212 var selectedUrl = selectedItem.getUrl(); |
| 213 // Show the selected item ASAP, then complete the initialization |
| 214 // (loading the ribbon thumbnails can take some time). |
| 215 this.metadataCache_.get(selectedUrl, Gallery.METADATA_TYPE, |
| 216 function(metadata) { |
| 217 this.loadItem_(selectedUrl, metadata, 0, loadDone); |
| 218 }.bind(this)); |
| 219 }; |
| 220 |
| 221 /** |
| 222 * Setup navigation controls. |
| 223 * @private |
| 224 */ |
| 225 SlideMode.prototype.setupNavigation_ = function() { |
| 226 this.sequenceDirection_ = 0; |
| 227 this.sequenceLength_ = 0; |
| 228 |
| 229 ImageUtil.setAttribute(this.arrowLeft_, 'active', this.items_.length > 1); |
| 230 ImageUtil.setAttribute(this.arrowRight_, 'active', this.items_.length > 1); |
| 231 |
| 232 this.ribbon_ = new Ribbon( |
| 233 this.document_, this.metadataCache_, this.select.bind(this)); |
| 234 this.ribbonSpacer_.appendChild(this.ribbon_); |
| 235 this.ribbon_.update(this.items_, this.selectedIndex_); |
| 236 }; |
| 237 |
| 238 /** |
| 239 * @return {Gallery.Item} Selected item |
| 240 */ |
| 241 SlideMode.prototype.getSelectedItem = function() { |
| 242 return this.items_ && this.items_[this.selectedIndex_]; |
| 243 }; |
| 244 |
| 245 /** |
| 246 * Change the selection. |
| 247 * |
| 248 * Commits the current image and changes the selection. |
| 249 * |
| 250 * @param {number} index New selected index. |
| 251 * @param {number} opt_slideDir Slide animation direction (-1|0|1). |
| 252 * Derived from the new and the old selected indices if omitted. |
| 253 * @param {function} opt_callback Callback. |
| 254 */ |
| 255 SlideMode.prototype.select = function(index, opt_slideDir, opt_callback) { |
| 256 if (!this.items_) |
| 257 return; // Not fully initialized, still loading the first image. |
| 258 |
| 259 if (index == this.selectedIndex_) |
| 260 return; // Do not reselect. |
| 261 |
| 262 this.commitItem_( |
| 263 this.doSelect_.bind(this, index, opt_slideDir, opt_callback)); |
| 264 }; |
| 265 |
| 266 /** |
| 267 * Set the new selected index value. |
| 268 * @param {number} index New selected index. |
| 269 * @private |
| 270 */ |
| 271 SlideMode.prototype.setSelectedIndex_ = function(index) { |
| 272 this.selectedIndex_ = index; |
| 273 cr.dispatchSimpleEvent(this, 'selection'); |
| 274 |
| 275 // For once edited image, disallow the 'overwrite' setting change. |
| 276 ImageUtil.setAttribute(this.options_, 'saved', |
| 277 !this.getSelectedItem().isOriginal()); |
| 278 }; |
| 279 |
| 280 /** |
| 281 * Perform the actual selection change. |
| 282 * |
| 283 * @param {number} index New selected index. |
| 284 * @param {number} opt_slideDir Slide animation direction (-1|0|1). |
| 285 * Derived from the new and the old selected indices if omitted. |
| 286 * @param {function} opt_callback Callback. |
| 287 * @private |
| 288 */ |
| 289 SlideMode.prototype.doSelect_ = function(index, opt_slideDir, opt_callback) { |
| 290 if (index == this.selectedIndex_) |
| 291 return; // Do not reselect |
| 292 |
| 293 var step = opt_slideDir != undefined ? |
| 294 opt_slideDir : |
| 295 (index - this.selectedIndex_); |
| 296 |
| 297 if (Math.abs(step) != 1) { |
| 298 // Long leap, the sequence is broken, we have no good prefetch candidate. |
| 299 this.sequenceDirection_ = 0; |
| 300 this.sequenceLength_ = 0; |
| 301 } else if (this.sequenceDirection_ == step) { |
| 302 // Keeping going in sequence. |
| 303 this.sequenceLength_++; |
| 304 } else { |
| 305 // Reversed the direction. Reset the counter. |
| 306 this.sequenceDirection_ = step; |
| 307 this.sequenceLength_ = 1; |
| 308 } |
| 309 |
| 310 if (this.sequenceLength_ <= 1) { |
| 311 // We have just broke the sequence. Touch the current image so that it stays |
| 312 // in the cache longer. |
| 313 this.editor_.prefetchImage(this.getSelectedItem().getUrl()); |
| 314 } |
| 315 |
| 316 this.setSelectedIndex_(index); |
| 317 |
| 318 if (this.ribbon_) |
| 319 this.ribbon_.update(this.items_, this.selectedIndex_); |
| 320 |
| 321 function shouldPrefetch(loadType, step, sequenceLength) { |
| 322 // Never prefetch when selecting out of sequence. |
| 323 if (Math.abs(step) != 1) |
| 324 return false; |
| 325 |
| 326 // Never prefetch after a video load (decoding the next image can freeze |
| 327 // the UI for a second or two). |
| 328 if (loadType == ImageView.LOAD_TYPE_VIDEO_FILE) |
| 329 return false; |
| 330 |
| 331 // Always prefetch if the previous load was from cache. |
| 332 if (loadType == ImageView.LOAD_TYPE_CACHED_FULL) |
| 333 return true; |
| 334 |
| 335 // Prefetch if we have been going in the same direction for long enough. |
| 336 return sequenceLength >= 3; |
| 337 } |
| 338 |
| 339 var selectedItem = this.getSelectedItem(); |
| 340 var onMetadata = function(metadata) { |
| 341 if (selectedItem != this.getSelectedItem()) return; |
| 342 this.loadItem_(selectedItem.getUrl(), metadata, step, |
| 343 function(loadType) { |
| 344 if (selectedItem != this.getSelectedItem()) return; |
| 345 if (shouldPrefetch(loadType, step, this.sequenceLength_)) { |
| 346 this.requestPrefetch(step); |
| 347 } |
| 348 if (opt_callback) opt_callback(); |
| 349 }.bind(this)); |
| 350 }.bind(this); |
| 351 this.metadataCache_.get( |
| 352 selectedItem.getUrl(), Gallery.METADATA_TYPE, onMetadata); |
| 353 }; |
| 354 |
| 355 /** |
| 356 * @param {number} direction -1 for left, 1 for right. |
| 357 * @return {number} Next index in the gived direction, with wrapping. |
| 358 * @private |
| 359 */ |
| 360 SlideMode.prototype.getNextSelectedIndex_ = function(direction) { |
| 361 var index = this.selectedIndex_ + (direction > 0 ? 1 : -1); |
| 362 if (index == -1) return this.items_.length - 1; |
| 363 if (index == this.items_.length) return 0; |
| 364 return index; |
| 365 }; |
| 366 |
| 367 /** |
| 368 * Select the next item. |
| 369 * @param {number} direction -1 for left, 1 for right. |
| 370 * @param {function} opt_callback Callback. |
| 371 */ |
| 372 SlideMode.prototype.selectNext = function(direction, opt_callback) { |
| 373 this.select(this.getNextSelectedIndex_(direction), direction, opt_callback); |
| 374 }; |
| 375 |
| 376 /** |
| 377 * Select the first item. |
| 378 */ |
| 379 SlideMode.prototype.selectFirst = function() { |
| 380 this.select(0); |
| 381 }; |
| 382 |
| 383 /** |
| 384 * Select the last item. |
| 385 */ |
| 386 SlideMode.prototype.selectLast = function() { |
| 387 this.select(this.items_.length - 1); |
| 388 }; |
| 389 |
| 390 // Loading/unloading |
| 391 |
| 392 /** |
| 393 * Load and display an item. |
| 394 * |
| 395 * @param {string} url Item url. |
| 396 * @param {Object} metadata Item metadata. |
| 397 * @param {number} slide Slide animation direction (-1|0|1). |
| 398 * @param {function} callback Callback. |
| 399 * @private |
| 400 */ |
| 401 SlideMode.prototype.loadItem_ = function(url, metadata, slide, callback) { |
| 402 this.selectedImageMetadata_ = ImageUtil.deepCopy(metadata); |
| 403 |
| 404 this.showSpinner_(true); |
| 405 |
| 406 var loadDone = function(loadType) { |
| 407 var video = this.isShowingVideo_(); |
| 408 ImageUtil.setAttribute(this.container_, 'video', video); |
| 409 |
| 410 this.showSpinner_(false); |
| 411 if (loadType == ImageView.LOAD_TYPE_ERROR) { |
| 412 this.showErrorBanner_(video ? 'VIDEO_ERROR' : 'IMAGE_ERROR'); |
| 413 } else if (loadType == ImageView.LOAD_TYPE_OFFLINE) { |
| 414 this.showErrorBanner_(video ? 'VIDEO_OFFLINE' : 'IMAGE_OFFLINE'); |
| 415 } |
| 416 |
| 417 if (video) { |
| 418 if (this.isEditing()) { |
| 419 // The editor toolbar does not make sense for video, hide it. |
| 420 this.onEdit_(); |
| 421 } |
| 422 this.mediaControls_.attachMedia(this.imageView_.getVideo()); |
| 423 //TODO(kaznacheev): Add metrics for video playback. |
| 424 } else { |
| 425 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View')); |
| 426 |
| 427 function toMillions(number) { return Math.round(number / (1000 * 1000)) } |
| 428 |
| 429 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'), |
| 430 toMillions(metadata.filesystem.size)); |
| 431 |
| 432 var canvas = this.imageView_.getCanvas(); |
| 433 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'), |
| 434 toMillions(canvas.width * canvas.height)); |
| 435 |
| 436 var extIndex = url.lastIndexOf('.'); |
| 437 var ext = extIndex < 0 ? '' : url.substr(extIndex + 1).toLowerCase(); |
| 438 if (ext == 'jpeg') ext = 'jpg'; |
| 439 ImageUtil.metrics.recordEnum( |
| 440 ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES); |
| 441 } |
| 442 |
| 443 callback(loadType); |
| 444 }.bind(this); |
| 445 |
| 446 this.editor_.openSession( |
| 447 url, metadata, slide, this.saveCurrentImage_.bind(this), loadDone); |
| 448 }; |
| 449 |
| 450 /** |
| 451 * Commit changes to the current item and reset all messages/indicators. |
| 452 * |
| 453 * @param {function} callback Callback. |
| 454 * @private |
| 455 */ |
| 456 SlideMode.prototype.commitItem_ = function(callback) { |
| 457 this.showSpinner_(false); |
| 458 this.showErrorBanner_(false); |
| 459 this.editor_.getPrompt().hide(); |
| 460 if (this.isShowingVideo_()) { |
| 461 this.mediaControls_.pause(); |
| 462 this.mediaControls_.detachMedia(); |
| 463 } |
| 464 this.editor_.closeSession(callback); |
| 465 }; |
| 466 |
| 467 /** |
| 468 * Request a prefetch for the next image. |
| 469 * |
| 470 * @param {number} direction -1 or 1. |
| 471 */ |
| 472 SlideMode.prototype.requestPrefetch = function(direction) { |
| 473 if (this.items_.length <= 1) return; |
| 474 |
| 475 var index = this.getNextSelectedIndex_(direction); |
| 476 var nextItemUrl = this.items_[index].getUrl(); |
| 477 |
| 478 var selectedItem = this.getSelectedItem(); |
| 479 this.metadataCache_.get(nextItemUrl, Gallery.METADATA_TYPE, |
| 480 function(metadata) { |
| 481 if (selectedItem != this.getSelectedItem()) return; |
| 482 this.editor_.prefetchImage(nextItemUrl); |
| 483 }.bind(this)); |
| 484 }; |
| 485 |
| 486 // Event handlers. |
| 487 |
| 488 /** |
| 489 * Unload handler. |
| 490 * @private |
| 491 */ |
| 492 SlideMode.prototype.onUnload_ = function() { |
| 493 this.saveVideoPosition_(); |
| 494 window.top.removeEventListener('beforeunload', this.onBeforeUnloadBound_); |
| 495 window.top.removeEventListener('unload', this.onTopUnloadBound_); |
| 496 }; |
| 497 |
| 498 /** |
| 499 * Top window unload handler. |
| 500 * @private |
| 501 */ |
| 502 SlideMode.prototype.onTopUnload_ = function() { |
| 503 this.saveVideoPosition_(); |
| 504 }; |
| 505 |
| 506 /** |
| 507 * Top window beforeunload handler. |
| 508 * @return {string} Message to show if there are unsaved changes. |
| 509 * @private |
| 510 */ |
| 511 SlideMode.prototype.onBeforeUnload_ = function() { |
| 512 if (this.editor_.isBusy()) |
| 513 return this.displayStringFunction_('unsaved_changes'); |
| 514 return null; |
| 515 }; |
| 516 |
| 517 /** |
| 518 * Click handler. |
| 519 * @private |
| 520 */ |
| 521 SlideMode.prototype.onClick_ = function() { |
| 522 if (this.isShowingVideo_()) |
| 523 this.mediaControls_.togglePlayStateWithFeedback(); |
| 524 }; |
| 525 |
| 526 /** |
| 527 * Keydown handler. |
| 528 * |
| 529 * @param {Event} event Event. |
| 530 * @return {boolean} True if handled. |
| 531 */ |
| 532 SlideMode.prototype.onKeyDown = function(event) { |
| 533 if (this.isEditing() && this.editor_.onKeyDown(event)) |
| 534 return true; |
| 535 |
| 536 switch (util.getKeyModifiers(event) + event.keyIdentifier) { |
| 537 case 'U+0020': // Space toggles the video playback. |
| 538 if (this.isShowingVideo_()) { |
| 539 this.mediaControls_.togglePlayStateWithFeedback(); |
| 540 } |
| 541 break; |
| 542 |
| 543 case 'U+0045': // 'e' toggles the editor |
| 544 this.onEdit_(); |
| 545 break; |
| 546 |
| 547 case 'U+001B': // Escape |
| 548 if (!this.isEditing()) |
| 549 return false; // Not handled. |
| 550 this.onEdit_(); |
| 551 break; |
| 552 |
| 553 case 'Ctrl-U+00DD': // Ctrl+] (cryptic on purpose). |
| 554 this.toggleSlideshow_(); |
| 555 break; |
| 556 |
| 557 case 'Home': |
| 558 this.selectFirst(); |
| 559 break; |
| 560 case 'End': |
| 561 this.selectLast(); |
| 562 break; |
| 563 case 'Left': |
| 564 this.selectNext(-1); |
| 565 break; |
| 566 case 'Right': |
| 567 this.selectNext(1); |
| 568 break; |
| 569 |
| 570 default: return false; |
| 571 } |
| 572 |
| 573 return true; |
| 574 }; |
| 575 |
| 576 /** |
| 577 * Resize handler. |
| 578 * @private |
| 579 */ |
| 580 SlideMode.prototype.onResize_ = function() { |
| 581 this.viewport_.sizeByFrameAndFit(this.container_); |
| 582 this.viewport_.repaint(); |
| 583 }; |
| 584 |
| 585 // Saving |
| 586 |
| 587 /** |
| 588 * Save the current image to a file. |
| 589 * |
| 590 * @param {function} callback Callback. |
| 591 * @private |
| 592 */ |
| 593 SlideMode.prototype.saveCurrentImage_ = function(callback) { |
| 594 var item = this.getSelectedItem(); |
| 595 var oldUrl = item.getUrl(); |
| 596 var canvas = this.imageView_.getCanvas(); |
| 597 |
| 598 this.showSpinner_(true); |
| 599 var metadataEncoder = ImageEncoder.encodeMetadata( |
| 600 this.selectedImageMetadata_.media, canvas, 1 /* quality */); |
| 601 |
| 602 this.selectedImageMetadata_ = ContentProvider.ConvertContentMetadata( |
| 603 metadataEncoder.getMetadata(), this.selectedImageMetadata_); |
| 604 |
| 605 item.saveToFile( |
| 606 this.context_.saveDirEntry, |
| 607 this.shouldOverwriteOriginal_(), |
| 608 canvas, |
| 609 metadataEncoder, |
| 610 function(success) { |
| 611 // TODO(kaznacheev): Implement write error handling. |
| 612 // Until then pretend that the save succeeded. |
| 613 this.showSpinner_(false); |
| 614 this.flashSavedLabel_(); |
| 615 var newUrl = item.getUrl(); |
| 616 this.updateSelectedUrl_(oldUrl, newUrl); |
| 617 this.ribbon_.updateThumbnail( |
| 618 this.selectedIndex_, newUrl, this.selectedImageMetadata_); |
| 619 callback(); |
| 620 }.bind(this)); |
| 621 }; |
| 622 |
| 623 /** |
| 624 * Update caches when the selected item url has changed. |
| 625 * |
| 626 * @param {string} oldUrl Old url. |
| 627 * @param {string} newUrl New url. |
| 628 * @private |
| 629 */ |
| 630 SlideMode.prototype.updateSelectedUrl_ = function(oldUrl, newUrl) { |
| 631 this.metadataCache_.clear(oldUrl, Gallery.METADATA_TYPE); |
| 632 |
| 633 if (oldUrl == newUrl) |
| 634 return; |
| 635 |
| 636 this.imageView_.changeUrl(newUrl); |
| 637 this.ribbon_.remapCache(oldUrl, newUrl); |
| 638 |
| 639 // Let the gallery know that the selected item url has changed. |
| 640 cr.dispatchSimpleEvent(this, 'selection'); |
| 641 }; |
| 642 |
| 643 /** |
| 644 * Flash 'Saved' label briefly to indicate that the image has been saved. |
| 645 * @private |
| 646 */ |
| 647 SlideMode.prototype.flashSavedLabel_ = function() { |
| 648 var setLabelHighlighted = |
| 649 ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted'); |
| 650 setTimeout(setLabelHighlighted.bind(null, true), 0); |
| 651 setTimeout(setLabelHighlighted.bind(null, false), 300); |
| 652 }; |
| 653 |
| 654 /** |
| 655 * Local storage key for the 'Overwrite original' setting. |
| 656 * @type {string} |
| 657 */ |
| 658 SlideMode.OVERWRITE_KEY = 'gallery-overwrite-original'; |
| 659 |
| 660 /** |
| 661 * Local storage key for the number of times that |
| 662 * the overwrite info bubble has been displayed. |
| 663 * @type {string} |
| 664 */ |
| 665 SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble'; |
| 666 |
| 667 /** |
| 668 * Max number that the overwrite info bubble is shown. |
| 669 * @type {number} |
| 670 */ |
| 671 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5; |
| 672 |
| 673 /** |
| 674 * @return {boolean} True if 'Overwrite original' is set. |
| 675 * @private |
| 676 */ |
| 677 SlideMode.prototype.shouldOverwriteOriginal_ = function() { |
| 678 return SlideMode.OVERWRITE_KEY in localStorage && |
| 679 (localStorage[SlideMode.OVERWRITE_KEY] == 'true'); |
| 680 }; |
| 681 |
| 682 /** |
| 683 * 'Overwrite original' checkbox handler. |
| 684 * @param {Event} event Event. |
| 685 * @private |
| 686 */ |
| 687 SlideMode.prototype.onOverwriteOriginalClick_ = function(event) { |
| 688 localStorage['gallery-overwrite-original'] = event.target.checked; |
| 689 }; |
| 690 |
| 691 /** |
| 692 * Overwrite info bubble close handler. |
| 693 * @private |
| 694 */ |
| 695 SlideMode.prototype.onCloseBubble_ = function() { |
| 696 this.bubble_.hidden = true; |
| 697 localStorage[SlideMode.OVERWRITE_BUBBLE_KEY] = |
| 698 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES; |
| 699 }; |
| 700 |
| 701 /** |
| 702 * Callback called when the image is edited. |
| 703 * @private |
| 704 */ |
| 705 SlideMode.prototype.onImageContentChanged_ = function() { |
| 706 var revision = this.imageView_.getContentRevision(); |
| 707 if (revision == 0) { |
| 708 // Just loaded. |
| 709 var times = SlideMode.OVERWRITE_BUBBLE_KEY in localStorage ? |
| 710 parseInt(localStorage[SlideMode.OVERWRITE_BUBBLE_KEY], 10) : 0; |
| 711 if (times < SlideMode.OVERWRITE_BUBBLE_MAX_TIMES) { |
| 712 this.bubble_.hidden = false; |
| 713 if (this.isEditing()) { |
| 714 localStorage[SlideMode.OVERWRITE_BUBBLE_KEY] = times + 1; |
| 715 } |
| 716 } |
| 717 } |
| 718 |
| 719 if (revision == 1) { |
| 720 // First edit. |
| 721 ImageUtil.setAttribute(this.options_, 'saved', true); |
| 722 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit')); |
| 723 } |
| 724 }; |
| 725 |
| 726 // Misc |
| 727 |
| 728 /** |
| 729 * Start/stop the slide show. |
| 730 * @private |
| 731 */ |
| 732 SlideMode.prototype.toggleSlideshow_ = function() { |
| 733 if (this.slideShowTimeout_) { |
| 734 clearInterval(this.slideShowTimeout_); |
| 735 this.slideShowTimeout_ = null; |
| 736 } else { |
| 737 var self = this; |
| 738 function nextSlide() { |
| 739 self.selectNext(1, |
| 740 function() { self.slideShowTimeout_ = setTimeout(nextSlide, 5000) }); |
| 741 } |
| 742 nextSlide(); |
| 743 } |
| 744 }; |
| 745 |
| 746 /** |
| 747 * @return {boolean} True if the editor is active. |
| 748 */ |
| 749 SlideMode.prototype.isEditing = function() { |
| 750 return this.container_.hasAttribute('editing'); |
| 751 }; |
| 752 |
| 753 /** |
| 754 * Activate/deactivate editor. |
| 755 * @private |
| 756 */ |
| 757 SlideMode.prototype.onEdit_ = function() { |
| 758 if (!this.isEditing() && this.isShowingVideo_()) |
| 759 return; // No editing for videos. |
| 760 |
| 761 ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing()); |
| 762 |
| 763 if (this.isEditing()) { // isEditing_ has just been flipped to a new value. |
| 764 if (this.context_.readonlyDirName) { |
| 765 this.editor_.getPrompt().showAt( |
| 766 'top', 'readonly_warning', 0, this.context_.readonlyDirName); |
| 767 } |
| 768 } else { |
| 769 this.editor_.getPrompt().hide(); |
| 770 } |
| 771 |
| 772 ImageUtil.setAttribute(this.editButton_, 'pressed', this.isEditing()); |
| 773 |
| 774 cr.dispatchSimpleEvent(this, 'edit'); |
| 775 }; |
| 776 |
| 777 /** |
| 778 * Display the error banner. |
| 779 * @param {string} message Message. |
| 780 * @private |
| 781 */ |
| 782 SlideMode.prototype.showErrorBanner_ = function(message) { |
| 783 if (message) { |
| 784 this.errorBanner_.textContent = this.displayStringFunction_(message); |
| 785 } |
| 786 ImageUtil.setAttribute(this.container_, 'error', !!message); |
| 787 }; |
| 788 |
| 789 /** |
| 790 * Show/hide the busy spinner. |
| 791 * |
| 792 * @param {boolean} on True if show, false if hide. |
| 793 * @private |
| 794 */ |
| 795 SlideMode.prototype.showSpinner_ = function(on) { |
| 796 if (this.spinnerTimer_) { |
| 797 clearTimeout(this.spinnerTimer_); |
| 798 this.spinnerTimer_ = null; |
| 799 } |
| 800 |
| 801 if (on) { |
| 802 this.spinnerTimer_ = setTimeout(function() { |
| 803 this.spinnerTimer_ = null; |
| 804 ImageUtil.setAttribute(this.container_, 'spinner', true); |
| 805 }.bind(this), 1000); |
| 806 } else { |
| 807 ImageUtil.setAttribute(this.container_, 'spinner', false); |
| 808 } |
| 809 }; |
| 810 |
| 811 /** |
| 812 * @return {boolean} True if the current item is a video. |
| 813 * @private |
| 814 */ |
| 815 SlideMode.prototype.isShowingVideo_ = function() { |
| 816 return !!this.imageView_.getVideo(); |
| 817 }; |
| 818 |
| 819 /** |
| 820 * Save the current video position. |
| 821 * @private |
| 822 */ |
| 823 SlideMode.prototype.saveVideoPosition_ = function() { |
| 824 if (this.isShowingVideo_() && this.mediaControls_.isPlaying()) { |
| 825 this.mediaControls_.savePosition(); |
| 826 } |
| 827 }; |
| 828 |
| 829 /** |
| 830 * Overlay that handles swipe gestures. Changes to the next or previous file. |
| 831 * @param {function(number)} callback A callback accepting the swipe direction |
| 832 * (1 means left, -1 right). |
| 833 * @constructor |
| 834 * @implements {ImageBuffer.Overlay} |
| 835 */ |
| 836 function SwipeOverlay(callback) { |
| 837 this.callback_ = callback; |
| 838 } |
| 839 |
| 840 /** |
| 841 * Inherit ImageBuffer.Overlay. |
| 842 */ |
| 843 SwipeOverlay.prototype.__proto__ = ImageBuffer.Overlay.prototype; |
| 844 |
| 845 /** |
| 846 * @param {number} x X pointer position. |
| 847 * @param {number} y Y pointer position. |
| 848 * @param {boolean} touch True if dragging caused by touch. |
| 849 * @return {function} The closure to call on drag. |
| 850 */ |
| 851 SwipeOverlay.prototype.getDragHandler = function(x, y, touch) { |
| 852 if (!touch) |
| 853 return null; |
| 854 var origin = x; |
| 855 var done = false; |
| 856 return function(x, y) { |
| 857 if (!done && origin - x > SwipeOverlay.SWIPE_THRESHOLD) { |
| 858 this.callback_(1); |
| 859 done = true; |
| 860 } else if (!done && x - origin > SwipeOverlay.SWIPE_THRESHOLD) { |
| 861 this.callback_(-1); |
| 862 done = true; |
| 863 } |
| 864 }.bind(this); |
| 865 }; |
| 866 |
| 867 /** |
| 868 * If the user touched the image and moved the finger more than SWIPE_THRESHOLD |
| 869 * horizontally it's considered as a swipe gesture (change the current image). |
| 870 */ |
| 871 SwipeOverlay.SWIPE_THRESHOLD = 100; |
OLD | NEW |