diff options
6 files changed, 215 insertions, 75 deletions
diff --git a/chrome/browser/resources/file_manager/css/gallery.css b/chrome/browser/resources/file_manager/css/gallery.css index bea91d1..8d820b2 100644 --- a/chrome/browser/resources/file_manager/css/gallery.css +++ b/chrome/browser/resources/file_manager/css/gallery.css @@ -1228,9 +1228,20 @@ body { top: 0; } -.mosaic-tile img { - -webkit-user-drag: none; - position: absolute; +.mosaic-tile .img-wrapper[generic-thumbnail], +.mosaic-tile .img-wrapper:not([generic-thumbnail]) canvas { + -webkit-animation: fadeIn ease-in 1; + -webkit-animation-duration: 500ms; + -webkit-animation-fill-mode: forwards; +} + +@-webkit-keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } } /* In order to do mode animated transitions smoothly we keep both mosaic and diff --git a/chrome/browser/resources/file_manager/css/photo_import.css b/chrome/browser/resources/file_manager/css/photo_import.css index 41a1254..91d4cc0 100644 --- a/chrome/browser/resources/file_manager/css/photo_import.css +++ b/chrome/browser/resources/file_manager/css/photo_import.css @@ -162,7 +162,7 @@ button.import { opacity: 0; } to { - opacity :1; + opacity: 1; } } diff --git a/chrome/browser/resources/file_manager/js/file_tasks.js b/chrome/browser/resources/file_manager/js/file_tasks.js index 5d404f7..e65d65a 100644 --- a/chrome/browser/resources/file_manager/js/file_tasks.js +++ b/chrome/browser/resources/file_manager/js/file_tasks.js @@ -458,7 +458,6 @@ FileTasks.prototype.openGallery = function(urls) { metadataCache: fm.metadataCache_, pageState: this.params_, onClose: onClose, - allowMosaic: fm.isOnDrive(), onThumbnailError: function(imageURL) { fm.metadataCache_.refreshFileMetadata(imageURL); }, diff --git a/chrome/browser/resources/file_manager/js/media/media_util.js b/chrome/browser/resources/file_manager/js/media/media_util.js index 07d7166..d73f529 100644 --- a/chrome/browser/resources/file_manager/js/media/media_util.js +++ b/chrome/browser/resources/file_manager/js/media/media_util.js @@ -12,9 +12,14 @@ * default: IMAGE. * @param {Object=} opt_metadata Metadata object. * @param {string=} opt_mediaType Media type. + * @param {ThumbnailLoader.UseEmbedded=} opt_useEmbedded If to use embedded + * jpeg thumbnail if available. Default: USE_EMBEDDED. * @constructor */ -function ThumbnailLoader(url, opt_loaderType, opt_metadata, opt_mediaType) { +function ThumbnailLoader( + url, opt_loaderType, opt_metadata, opt_mediaType, opt_useEmbedded) { + opt_useEmbedded = opt_useEmbedded || ThumbnailLoader.UseEmbedded.USE_EMBEDDED; + this.mediaType_ = opt_mediaType || FileType.getMediaType(url); this.loaderType_ = opt_loaderType || ThumbnailLoader.LoaderType.IMAGE; this.metadata_ = opt_metadata; @@ -36,7 +41,8 @@ function ThumbnailLoader(url, opt_loaderType, opt_metadata, opt_mediaType) { } } - if (opt_metadata.thumbnail && opt_metadata.thumbnail.url) { + if (opt_metadata.thumbnail && opt_metadata.thumbnail.url && + opt_useEmbedded == ThumbnailLoader.UseEmbedded.USE_EMBEDDED) { this.thumbnailUrl_ = opt_metadata.thumbnail.url; this.transform_ = opt_metadata.thumbnail.transform; } else if (FileType.isImage(url)) { @@ -59,7 +65,7 @@ ThumbnailLoader.AUTO_FILL_THRESHOLD = 0.3; /** * Type of displaying a thumbnail within a box. - * @enum + * @enum {number} */ ThumbnailLoader.FillMode = { FILL: 0, // Fill whole box. Image may be cropped. @@ -69,7 +75,7 @@ ThumbnailLoader.FillMode = { /** * Optimization mode for downloading thumbnails. - * @enum + * @enum {number} */ ThumbnailLoader.OptimizationMode = { NEVER_DISCARD: 0, // Never discards downloading. No optimization. @@ -78,7 +84,7 @@ ThumbnailLoader.OptimizationMode = { /** * Type of element to store the image. - * @enum + * @enum {number} */ ThumbnailLoader.LoaderType = { IMAGE: 0, @@ -86,6 +92,16 @@ ThumbnailLoader.LoaderType = { }; /** + * Whether to use the embedded thumbnail, or not. The embedded thumbnail may + * be small. + * @enum {number} + */ +ThumbnailLoader.UseEmbedded = { + USE_EMBEDDED: 0, + NO_EMBEDDED: 1 +}; + +/** * Maximum thumbnail's width when generating from the full resolution image. * @const * @type {number} @@ -123,6 +139,7 @@ ThumbnailLoader.prototype.load = function(box, fillMode, opt_optimizationMode, return; } + this.cancel(); this.canvasUpToDate_ = false; this.image_ = new Image(); this.image_.onload = function() { @@ -155,7 +172,7 @@ ThumbnailLoader.prototype.load = function(box, fillMode, opt_optimizationMode, this.metadata_.filesystem && this.metadata_.filesystem.modificationTime && this.metadata_.filesystem.modificationTime.getTime(); - var taskId = util.loadImage( + this.taskId_ = util.loadImage( this.image_, this.thumbnailUrl_, { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH, @@ -172,11 +189,22 @@ ThumbnailLoader.prototype.load = function(box, fillMode, opt_optimizationMode, return true; }); - if (!taskId) + if (!this.taskId_) this.image_.classList.add('cached'); }; /** + * Cancels loading the current image. + */ +ThumbnailLoader.prototype.cancel = function() { + if (this.taskId_) { + this.image_.onload = function() {}; + this.image_.onerror = function() {}; + util.cancelLoadImage(this.taskId_); + } +}; + +/** * @return {boolean} True if a valid image is loaded. */ ThumbnailLoader.prototype.hasValidImage = function() { @@ -217,6 +245,7 @@ ThumbnailLoader.prototype.loadDetachedImage = function(callback) { return; } + this.cancel(); this.canvasUpToDate_ = false; this.image_ = new Image(); this.image_.onload = callback.bind(null, true); @@ -227,7 +256,7 @@ ThumbnailLoader.prototype.loadDetachedImage = function(callback) { this.metadata_.filesystem && this.metadata_.filesystem.modificationTime && this.metadata_.filesystem.modificationTime.getTime(); - var taskId = util.loadImage( + this.taskId_ = util.loadImage( this.image_, this.thumbnailUrl_, { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH, @@ -235,7 +264,7 @@ ThumbnailLoader.prototype.loadDetachedImage = function(callback) { cache: true, timestamp: modificationTime }); - if (!taskId) + if (!this.taskId_) this.image_.classList.add('cached'); }; @@ -274,7 +303,7 @@ ThumbnailLoader.prototype.attachImage = function(container, fillMode) { util.applyTransform(container, this.transform_); ThumbnailLoader.centerImage_( container, attachableMedia, fillMode, this.isRotated_()); - if (this.image_.parentNode != container) { + if (attachableMedia.parentNode != container) { container.textContent = ''; container.appendChild(attachableMedia); } diff --git a/chrome/browser/resources/file_manager/js/photo/gallery.js b/chrome/browser/resources/file_manager/js/photo/gallery.js index a27e5d4..a330743 100644 --- a/chrome/browser/resources/file_manager/js/photo/gallery.js +++ b/chrome/browser/resources/file_manager/js/photo/gallery.js @@ -137,7 +137,6 @@ Gallery.openStandalone = function(path, pageState, opt_callback) { metadataCache: MetadataCache.createFull(), pageState: pageState, onClose: onClose, - allowMosaic: true, /* For debugging purposes */ displayStringFunction: strf }; Gallery.open(context, urls, selectedUrls); @@ -148,18 +147,30 @@ Gallery.openStandalone = function(path, pageState, opt_callback) { /** * Tools fade-out timeout im milliseconds. + * @const * @type {number} */ Gallery.FADE_TIMEOUT = 3000; /** * First time tools fade-out timeout im milliseconds. + * @const * @type {number} */ Gallery.FIRST_FADE_TIMEOUT = 1000; /** + * Time until mosaic is initialized in the background. Used to make gallery + * in the slide mode load faster. In miiliseconds. + * @const + * @type {number} + */ +Gallery.MOSAIC_BACKGROUND_INIT_DELAY = 1000; + +/** * Types of metadata Gallery uses (to query the metadata cache). + * @const + * @type {string} */ Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|streaming'; @@ -167,7 +178,6 @@ Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|streaming'; * Initialize listeners. * @private */ - Gallery.prototype.initListeners_ = function() { if (!util.TEST_HARNESS) this.document_.oncontextmenu = function(e) { e.preventDefault(); }; @@ -259,15 +269,13 @@ Gallery.prototype.initDom_ = function() { var onThumbnailError = this.context_.onThumbnailError || function() {}; - if (this.context_.allowMosaic) { - this.modeButton_ = util.createChild(this.toolbar_, 'button mode', 'button'); - this.modeButton_.addEventListener('click', - this.toggleMode_.bind(this, null)); + this.modeButton_ = util.createChild(this.toolbar_, 'button mode', 'button'); + this.modeButton_.addEventListener('click', + this.toggleMode_.bind(this, null)); - this.mosaicMode_ = new MosaicMode(content, - this.dataModel_, this.selectionModel_, this.metadataCache_, - this.toggleMode_.bind(this, null), onThumbnailError); - } + this.mosaicMode_ = new MosaicMode(content, + this.dataModel_, this.selectionModel_, this.metadataCache_, + this.toggleMode_.bind(this, null), onThumbnailError); this.slideMode_ = new SlideMode(this.container_, content, this.toolbar_, this.prompt_, @@ -363,7 +371,8 @@ Gallery.prototype.load = function(urls, selectedUrls) { } else { this.setCurrentMode_(this.slideMode_); var maybeLoadMosaic = function() { - if (mosaic) mosaic.init(); + if (mosaic) + mosaic.init(); cr.dispatchSimpleEvent(this, 'loaded'); }.bind(this); /* TODO: consider nice blow-up animation for the first image */ diff --git a/chrome/browser/resources/file_manager/js/photo/mosaic_mode.js b/chrome/browser/resources/file_manager/js/photo/mosaic_mode.js index 16036d0..e1dd380 100644 --- a/chrome/browser/resources/file_manager/js/photo/mosaic_mode.js +++ b/chrome/browser/resources/file_manager/js/photo/mosaic_mode.js @@ -85,12 +85,16 @@ Mosaic.prototype.__proto__ = HTMLDivElement.prototype; /** * Default layout delay in ms. + * @const + * @type {number} */ Mosaic.LAYOUT_DELAY = 200; /** * Smooth scroll animation duration when scrolling using keyboard or * clicking on a partly visible tile. In ms. + * @const + * @type {number} */ Mosaic.ANIMATED_SCROLL_DURATION = 500; @@ -137,7 +141,7 @@ Mosaic.prototype.init = function() { this.tiles_[index].select(true); }.bind(this)); - this.loadTiles_(this.tiles_); + this.initTiles_(this.tiles_); // The listeners might be called while some tiles are still loading. this.initListeners_(); @@ -166,6 +170,7 @@ Mosaic.prototype.initListeners_ = function() { this.addEventListener('mousemove', mouseEventBound); this.addEventListener('mousedown', mouseEventBound); this.addEventListener('mouseup', mouseEventBound); + this.addEventListener('scroll', this.onScroll_.bind(this)); this.selectionModel_.addEventListener('change', this.onSelection_.bind(this)); this.selectionModel_.addEventListener('leadIndexChange', @@ -261,13 +266,13 @@ Mosaic.prototype.scrollIntoView = function(index) { }; /** - * Load multiple tiles. + * Initializes multiple tiles. * * @param {Array.<Mosaic.Tile>} tiles Array of tiles. - * @param {function=} opt_callback Completion callback. + * @param {function()=} opt_callback Completion callback. * @private */ -Mosaic.prototype.loadTiles_ = function(tiles, opt_callback) { +Mosaic.prototype.initTiles_ = function(tiles, opt_callback) { // We do not want to use tile indices in asynchronous operations because they // do not survive data model splices. Copy tile references instead. tiles = tiles.slice(); @@ -283,7 +288,7 @@ Mosaic.prototype.loadTiles_ = function(tiles, opt_callback) { var chunkSize = Math.min(tiles.length, MAX_CHUNK_SIZE); var loaded = 0; for (var i = 0; i != chunkSize; i++) { - this.loadTile_(tiles.shift(), function() { + this.initTile_(tiles.shift(), function() { if (++loaded == chunkSize) { this.layout(); loadChunk(); @@ -296,22 +301,19 @@ Mosaic.prototype.loadTiles_ = function(tiles, opt_callback) { }; /** - * Load a single tile. + * Initializes a single tile. * * @param {Mosaic.Tile} tile Tile. - * @param {function} callback Completion callback. + * @param {function()} callback Completion callback. * @private */ -Mosaic.prototype.loadTile_ = function(tile, callback) { +Mosaic.prototype.initTile_ = function(tile, callback) { var url = tile.getItem().getUrl(); - var onImageLoaded = function(success) { - if (!success && this.onThumbnailError_) { - this.onThumbnailError_(url); - } - callback(); - }.bind(this); + var onImageMeasured = callback; this.metadataCache_.get(url, Gallery.METADATA_TYPE, - function(metadata) { tile.load(metadata, onImageLoaded) }); + function(metadata) { + tile.init(metadata, onImageMeasured); + }); }; /** @@ -320,7 +322,7 @@ Mosaic.prototype.loadTile_ = function(tile, callback) { Mosaic.prototype.reload = function() { this.layoutModel_.reset_(); this.tiles_.forEach(function(t) { t.markUnloaded() }); - this.loadTiles_(this.tiles_); + this.initTiles_(this.tiles_); }; /** @@ -339,10 +341,11 @@ Mosaic.prototype.layout = function() { if (index == this.tiles_.length) break; // All tiles done. var tile = this.tiles_[index]; - if (!tile.isLoaded()) + if (!tile.isInitialized()) break; // Next layout will try to restart from here. this.layoutModel_.add(tile, index + 1 == this.tiles_.length); } + this.loadVisibleTiles_(); }; /** @@ -397,6 +400,14 @@ Mosaic.prototype.onMouseEvent_ = function(event) { }; /** + * Scroll handler. + * @private + */ +Mosaic.prototype.onScroll_ = function() { + this.loadVisibleTiles_(); +}; + +/** * Selection change handler. * * @param {Event} event Event. @@ -448,7 +459,7 @@ Mosaic.prototype.onSplice_ = function(event) { newTiles.push(new Mosaic.Tile(this, this.dataModel_.item(index + t))); this.tiles_.splice.apply(this.tiles_, [index, 0].concat(newTiles)); - this.loadTiles_(newTiles); + this.initTiles_(newTiles); } if (this.tiles_.length != this.dataModel_.length) @@ -473,8 +484,11 @@ Mosaic.prototype.onContentChange_ = function(event) { console.error('Content changed for unselected item'); this.layoutModel_.invalidateFromTile_(index); - this.tiles_[index].load( - event.metadata, this.scheduleLayout.bind(this, Mosaic.LAYOUT_DELAY)); + this.tiles_[index].init(event.metadata, function() { + this.tiles_[index].load( + this.scheduleLayout.bind(this, Mosaic.LAYOUT_DELAY), + this.onThumbnailError_); + }.bind(this)); }; /** @@ -528,6 +542,29 @@ Mosaic.prototype.hide = function() { }; /** + * Loads visible tiles. Ignores consecutive calls. Does not reload already + * loaded images. + * @private + */ +Mosaic.prototype.loadVisibleTiles_ = function() { + if (this.loadVisibleTilesTimer_) { + clearTimeout(this.loadVisibleTilesTimer_); + this.loadVisibleTilesTimer_ = null; + } + this.loadVisibleTilesTimer_ = setTimeout(function() { + var viewportRect = new Rect(0, 0, this.clientWidth, this.clientHeight); + for (var index = 0; index < this.tiles_.length; index++) { + var tile = this.tiles_[index]; + var imageRect = tile.getImageRect(); + if (!tile.isLoading() && !tile.isLoaded() && imageRect && + imageRect.intersects(viewportRect)) { + tile.load(function() {}, this.onThumbnailError_); + } + } + }.bind(this), 100); +}; + +/** * Apply or reset the zoom transform. * * @param {Rect} tileRect Tile rectangle. Reset the transform if null. @@ -1588,54 +1625,107 @@ Mosaic.Tile.prototype.getMaxContentHeight = function() { Mosaic.Tile.prototype.getAspectRatio = function() { return this.aspectRatio_ }; /** + * @return {boolean} True if the tile is initialized. + */ +Mosaic.Tile.prototype.isInitialized = function() { + return !!this.maxContentHeight_; +}; + +/** * @return {boolean} True if the tile is loaded. */ -Mosaic.Tile.prototype.isLoaded = function() { return !!this.maxContentHeight_ }; +Mosaic.Tile.prototype.isLoaded = function() { + return this.imageLoaded_; +}; + +/** + * @return {boolean} True if the tile is being loaded. + */ +Mosaic.Tile.prototype.isLoading = function() { + return this.imageLoading_; +}; /** * Mark the tile as not loaded to prevent it from participating in the layout. */ Mosaic.Tile.prototype.markUnloaded = function() { this.maxContentHeight_ = 0; + if (this.thumbnailLoader_) { + this.thumbnailLoader_.cancel(); + this.imageLoaded_ = false; + this.imageLoading_ = false; + } }; /** - * Load the thumbnail image into the tile. + * Initializes the thumbnail in the tile. Does not load an image, but sets + * target dimensions using metadata. * * @param {Object} metadata Metadata object. - * @param {function} callback Completion callback. + * @param {function()} onImageMeasured Image measured callback. */ -Mosaic.Tile.prototype.load = function(metadata, callback) { +Mosaic.Tile.prototype.init = function(metadata, onImageMeasured) { this.markUnloaded(); this.left_ = null; // Mark as not laid out. - this.thumbnailLoader_ = new ThumbnailLoader(this.getItem().getUrl(), - ThumbnailLoader.LoaderType.CANVAS, - metadata); - - this.thumbnailLoader_.loadDetachedImage(function(success) { - if (this.thumbnailLoader_.hasValidImage()) { - var width = this.thumbnailLoader_.getWidth(); - var height = this.thumbnailLoader_.getHeight(); - if (width > height) { - if (width > Mosaic.Tile.MAX_CONTENT_SIZE) { - height = Math.round(height * Mosaic.Tile.MAX_CONTENT_SIZE / width); - width = Mosaic.Tile.MAX_CONTENT_SIZE; - } - } else { - if (height > Mosaic.Tile.MAX_CONTENT_SIZE) { - width = Math.round(width * Mosaic.Tile.MAX_CONTENT_SIZE / height); - height = Mosaic.Tile.MAX_CONTENT_SIZE; - } + this.thumbnailLoader_ = new ThumbnailLoader( + this.getItem().getUrl(), + ThumbnailLoader.LoaderType.CANVAS, + metadata, + undefined, // Media type. + ThumbnailLoader.UseEmbedded.NO_EMBEDDED); + + var setDimensions = function(width, height) { + if (width > height) { + if (width > Mosaic.Tile.MAX_CONTENT_SIZE) { + height = Math.round(height * Mosaic.Tile.MAX_CONTENT_SIZE / width); + width = Mosaic.Tile.MAX_CONTENT_SIZE; } - this.maxContentHeight_ = Math.max(Mosaic.Tile.MIN_CONTENT_SIZE, height); - this.aspectRatio_ = width / height; } else { - this.maxContentHeight_ = Mosaic.Tile.GENERIC_ICON_SIZE; - this.aspectRatio_ = 1; + if (height > Mosaic.Tile.MAX_CONTENT_SIZE) { + width = Math.round(width * Mosaic.Tile.MAX_CONTENT_SIZE / height); + height = Mosaic.Tile.MAX_CONTENT_SIZE; + } } + this.maxContentHeight_ = Math.max(Mosaic.Tile.MIN_CONTENT_SIZE, height); + this.aspectRatio_ = width / height; + onImageMeasured(); + }.bind(this); - callback(success); + // Dimensions are always acquired from the metadata. If it is not available, + // then the image will not be displayed. + if (metadata.media && metadata.media.width) { + setDimensions(metadata.media.width, metadata.media.height); + } else { + // No dimensions in metadata, then display the generic icon instead. + // TODO(mtomasz): Display a gneric icon instead of a black rectangle. + setDimensions(Mosaic.Tile.GENERIC_ICON_SIZE, + Mosaic.Tile.GENERIC_ICON_SIZE); + } +}; + +/** + * Loads an image into the tile. + * + * @param {function(boolean)} onImageLoaded Callback when image is loaded. + * The argument is true for success, false for failure. + * @param {function()=} opt_onThumbnailError Callback for image loading error. + */ +Mosaic.Tile.prototype.load = function(onImageLoaded, opt_onThumbnailError) { + this.imageLoaded_ = false; + this.imageLoading_ = true; + this.thumbnailLoader_.loadDetachedImage(function(success) { + if (!success) { + if (opt_onThumbnailError) + opt_onThumbnailError(); + } + if (this.wrapper_) { + this.thumbnailLoader_.attachImage(this.wrapper_, + ThumbnailLoader.FillMode.FILL); + } + onImageLoaded(success); + this.imageLoaded_ = true; + this.imageLoading_ = false; }.bind(this)); }; @@ -1678,8 +1768,10 @@ Mosaic.Tile.prototype.layout = function(left, top, width, height) { if (this.hasAttribute('selected')) this.scrollIntoView(false); - this.thumbnailLoader_.attachImage(this.wrapper_, - ThumbnailLoader.FillMode.FILL); + if (this.imageLoaded_) { + this.thumbnailLoader_.attachImage(this.wrapper_, + ThumbnailLoader.FillMode.FILL); + } }; /** |