summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--chrome/browser/resources/file_manager/css/gallery.css17
-rw-r--r--chrome/browser/resources/file_manager/css/photo_import.css2
-rw-r--r--chrome/browser/resources/file_manager/js/file_tasks.js1
-rw-r--r--chrome/browser/resources/file_manager/js/media/media_util.js49
-rw-r--r--chrome/browser/resources/file_manager/js/photo/gallery.js31
-rw-r--r--chrome/browser/resources/file_manager/js/photo/mosaic_mode.js190
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);
+ }
};
/**